2026-04-10 20:49:41 +03:00
|
|
|
|
mod support;
|
|
|
|
|
|
|
|
|
|
|
|
use std::env;
|
|
|
|
|
|
use std::fs;
|
|
|
|
|
|
|
|
|
|
|
|
use reqwest::blocking::Client;
|
|
|
|
|
|
use serde_json::Value;
|
|
|
|
|
|
|
|
|
|
|
|
use support::*;
|
|
|
|
|
|
|
|
|
|
|
|
const POLICY_E2E_YAML: &str = r#"
|
|
|
|
|
|
version: 1
|
|
|
|
|
|
groups:
|
|
|
|
|
|
team: [act-bruno]
|
|
|
|
|
|
admins: [act-ragnor]
|
|
|
|
|
|
protected_branches: [main]
|
|
|
|
|
|
rules:
|
|
|
|
|
|
- id: team-read
|
|
|
|
|
|
allow:
|
|
|
|
|
|
actors: { group: team }
|
|
|
|
|
|
actions: [read]
|
|
|
|
|
|
branch_scope: any
|
|
|
|
|
|
- id: team-write-unprotected
|
|
|
|
|
|
allow:
|
|
|
|
|
|
actors: { group: team }
|
|
|
|
|
|
actions: [change]
|
|
|
|
|
|
branch_scope: unprotected
|
|
|
|
|
|
- id: admins-promote
|
|
|
|
|
|
allow:
|
|
|
|
|
|
actors: { group: admins }
|
2026-04-30 08:52:50 +02:00
|
|
|
|
actions: [branch_merge]
|
2026-04-10 20:49:41 +03:00
|
|
|
|
target_branch_scope: protected
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
- id: admins-write
|
|
|
|
|
|
allow:
|
|
|
|
|
|
actors: { group: admins }
|
|
|
|
|
|
actions: [change]
|
|
|
|
|
|
branch_scope: any
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
- id: admins-branch-ops
|
|
|
|
|
|
allow:
|
|
|
|
|
|
actors: { group: admins }
|
|
|
|
|
|
actions: [branch_create, branch_delete]
|
|
|
|
|
|
target_branch_scope: any
|
|
|
|
|
|
- id: admins-schema-apply
|
|
|
|
|
|
allow:
|
|
|
|
|
|
actors: { group: admins }
|
|
|
|
|
|
actions: [schema_apply]
|
|
|
|
|
|
target_branch_scope: any
|
2026-04-10 20:49:41 +03:00
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
|
|
const POLICY_E2E_TESTS_YAML: &str = r#"
|
|
|
|
|
|
version: 1
|
|
|
|
|
|
cases:
|
|
|
|
|
|
- id: deny-main-change
|
|
|
|
|
|
actor: act-bruno
|
|
|
|
|
|
action: change
|
|
|
|
|
|
branch: main
|
|
|
|
|
|
expect: deny
|
|
|
|
|
|
- id: allow-feature-change
|
|
|
|
|
|
actor: act-bruno
|
|
|
|
|
|
action: change
|
|
|
|
|
|
branch: feature
|
|
|
|
|
|
expect: allow
|
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
|
|
fn yaml_string(value: &str) -> String {
|
|
|
|
|
|
format!("'{}'", value.replace('\'', "''"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
fn local_policy_config(graph: &SystemGraph) -> String {
|
2026-04-10 20:49:41 +03:00
|
|
|
|
format!(
|
|
|
|
|
|
"\
|
|
|
|
|
|
project:
|
|
|
|
|
|
name: policy-e2e-local
|
2026-04-14 04:12:14 +03:00
|
|
|
|
graphs:
|
2026-04-10 20:49:41 +03:00
|
|
|
|
local:
|
|
|
|
|
|
uri: {}
|
|
|
|
|
|
cli:
|
2026-04-14 04:12:14 +03:00
|
|
|
|
graph: local
|
2026-04-10 20:49:41 +03:00
|
|
|
|
branch: main
|
|
|
|
|
|
query:
|
|
|
|
|
|
roots:
|
|
|
|
|
|
- .
|
|
|
|
|
|
policy:
|
|
|
|
|
|
file: ./policy.yaml
|
|
|
|
|
|
",
|
2026-05-24 16:46:00 +01:00
|
|
|
|
yaml_string(&graph.path().to_string_lossy())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
fn insert_person_query(graph: &SystemGraph, name: &str) -> std::path::PathBuf {
|
|
|
|
|
|
graph.write_query(
|
2026-04-10 20:49:41 +03:00
|
|
|
|
name,
|
|
|
|
|
|
r#"
|
|
|
|
|
|
query insert_person($name: String, $age: I32) {
|
|
|
|
|
|
insert Person { name: $name, age: $age }
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
fn add_friend_query(graph: &SystemGraph, name: &str) -> std::path::PathBuf {
|
|
|
|
|
|
graph.write_query(
|
2026-04-10 20:49:41 +03:00
|
|
|
|
name,
|
|
|
|
|
|
r#"
|
|
|
|
|
|
query add_friend($from: String, $to: String) {
|
|
|
|
|
|
insert Knows { from: $from, to: $to }
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
fn snapshot_table_row_count(graph: &SystemGraph, table_key: &str) -> u64 {
|
|
|
|
|
|
snapshot_table_row_count_at(graph.path(), table_key)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
fn snapshot_table_row_count_at(graph: &std::path::Path, table_key: &str) -> u64 {
|
2026-04-10 20:49:41 +03:00
|
|
|
|
let payload = parse_stdout_json(&output_success(
|
2026-05-24 16:46:00 +01:00
|
|
|
|
cli().arg("snapshot").arg(graph).arg("--json"),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
));
|
|
|
|
|
|
payload["tables"]
|
|
|
|
|
|
.as_array()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|table| table["table_key"] == table_key)
|
|
|
|
|
|
.unwrap()["row_count"]
|
|
|
|
|
|
.as_u64()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn gemini_base_url() -> String {
|
|
|
|
|
|
env::var("OMNIGRAPH_GEMINI_BASE_URL")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.filter(|value| !value.trim().is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| "https://generativelanguage.googleapis.com/v1beta".to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn embed_text_with_gemini(text: &str, dim: usize) -> Vec<f32> {
|
|
|
|
|
|
let api_key = env::var("GEMINI_API_KEY").expect("GEMINI_API_KEY must be set");
|
|
|
|
|
|
let client = Client::new();
|
|
|
|
|
|
let response = client
|
|
|
|
|
|
.post(format!(
|
|
|
|
|
|
"{}/models/gemini-embedding-2-preview:embedContent",
|
|
|
|
|
|
gemini_base_url().trim_end_matches('/')
|
|
|
|
|
|
))
|
|
|
|
|
|
.header("x-goog-api-key", api_key)
|
|
|
|
|
|
.json(&serde_json::json!({
|
|
|
|
|
|
"model": "models/gemini-embedding-2-preview",
|
|
|
|
|
|
"content": {
|
|
|
|
|
|
"parts": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"text": text
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
"taskType": "RETRIEVAL_QUERY",
|
|
|
|
|
|
"outputDimensionality": dim,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.send()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.error_for_status()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.json::<Value>()
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
response["embedding"]["values"]
|
|
|
|
|
|
.as_array()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|value| value.as_f64().unwrap() as f32)
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn format_vector(values: &[f32]) -> String {
|
|
|
|
|
|
values
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|value| format!("{:.8}", value))
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join(", ")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
fn s3_test_graph_uri(suite: &str) -> Option<String> {
|
2026-04-10 20:49:41 +03:00
|
|
|
|
let bucket = env::var("OMNIGRAPH_S3_TEST_BUCKET").ok()?;
|
|
|
|
|
|
let prefix = env::var("OMNIGRAPH_S3_TEST_PREFIX")
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.filter(|value| !value.trim().is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| "omnigraph-itests".to_string());
|
|
|
|
|
|
let unique = std::time::SystemTime::now()
|
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
|
.ok()?
|
|
|
|
|
|
.as_nanos();
|
|
|
|
|
|
Some(format!("s3://{}/{}/{}/{}", bucket, prefix, suite, unique))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_end_to_end_init_load_read_change_read_flow() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::initialized();
|
|
|
|
|
|
let mutation_file = insert_person_query(&graph, "system-local-init-change.gq");
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("load")
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(fixture("test.jsonl"))
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path()),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let read_before = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(read_before["row_count"], 1);
|
|
|
|
|
|
assert_eq!(read_before["rows"][0]["p.name"], "Alice");
|
|
|
|
|
|
|
|
|
|
|
|
let change_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Eve","age":29}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(change_payload["branch"], "main");
|
|
|
|
|
|
assert_eq!(change_payload["affected_nodes"], 1);
|
|
|
|
|
|
|
|
|
|
|
|
let read_after = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Eve"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(read_after["row_count"], 1);
|
|
|
|
|
|
assert_eq!(read_after["rows"][0]["p.name"], "Eve");
|
feat: inline query strings in CLI and HTTP server (#110)
* 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>
2026-05-29 13:41:54 +02:00
|
|
|
|
|
|
|
|
|
|
// Inline-source variants of the same read/change flow (CLI `-e` /
|
|
|
|
|
|
// `--query-string`). Confirms that file-less invocations reach the
|
|
|
|
|
|
// engine identically, including param binding and `branch=main` defaults.
|
|
|
|
|
|
let inline_change = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
|
|
|
|
|
.arg(graph.path())
|
|
|
|
|
|
.arg("-e")
|
|
|
|
|
|
.arg("query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Inline","age":42}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(inline_change["branch"], "main");
|
|
|
|
|
|
assert_eq!(inline_change["query_name"], "add");
|
|
|
|
|
|
assert_eq!(inline_change["affected_nodes"], 1);
|
|
|
|
|
|
|
|
|
|
|
|
let inline_read = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
|
|
|
|
|
.arg(graph.path())
|
|
|
|
|
|
.arg("--query-string")
|
|
|
|
|
|
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Inline"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(inline_read["row_count"], 1);
|
|
|
|
|
|
assert_eq!(inline_read["rows"][0]["p.name"], "Inline");
|
|
|
|
|
|
assert_eq!(inline_read["rows"][0]["p.age"], 42);
|
2026-04-10 20:49:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_end_to_end_branch_change_merge_flow() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let mutation_file = insert_person_query(&graph, "system-local-change.gq");
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("create")
|
|
|
|
|
|
.arg("--uri")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--from")
|
|
|
|
|
|
.arg("main")
|
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let change_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Zoe","age":33}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(change_payload["branch"], "feature");
|
|
|
|
|
|
assert_eq!(change_payload["affected_nodes"], 1);
|
|
|
|
|
|
|
|
|
|
|
|
let feature_read = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Zoe"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(feature_read["row_count"], 1);
|
|
|
|
|
|
assert_eq!(feature_read["rows"][0]["p.name"], "Zoe");
|
|
|
|
|
|
|
|
|
|
|
|
let merge_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("merge")
|
|
|
|
|
|
.arg("--uri")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("feature")
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(merge_payload["target"], "main");
|
|
|
|
|
|
|
|
|
|
|
|
let main_read = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Zoe"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(main_read["row_count"], 1);
|
|
|
|
|
|
assert_eq!(main_read["rows"][0]["p.name"], "Zoe");
|
|
|
|
|
|
|
2026-05-01 22:45:38 +02:00
|
|
|
|
// `omnigraph run list` removed. Audit visible via commit list.
|
2026-04-30 08:52:50 +02:00
|
|
|
|
let commits_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("commit")
|
|
|
|
|
|
.arg("list")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-30 08:52:50 +02:00
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("main")
|
|
|
|
|
|
.arg("--json"),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
));
|
2026-04-30 08:52:50 +02:00
|
|
|
|
assert!(commits_payload["commits"].as_array().unwrap().len() >= 2);
|
2026-04-10 20:49:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_ingest_creates_review_branch_and_keeps_it_readable() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let ingest_data = graph.write_jsonl(
|
2026-04-10 20:49:41 +03:00
|
|
|
|
"system-local-ingest.jsonl",
|
|
|
|
|
|
r#"{"type":"Person","data":{"name":"Zoe","age":33}}
|
|
|
|
|
|
{"type":"Person","data":{"name":"Bob","age":26}}"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let ingest_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("ingest")
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&ingest_data)
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature-ingest")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(ingest_payload["branch"], "feature-ingest");
|
|
|
|
|
|
assert_eq!(ingest_payload["base_branch"], "main");
|
|
|
|
|
|
assert_eq!(ingest_payload["branch_created"], true);
|
|
|
|
|
|
assert_eq!(ingest_payload["mode"], "merge");
|
|
|
|
|
|
assert_eq!(ingest_payload["tables"][0]["table_key"], "node:Person");
|
|
|
|
|
|
assert_eq!(ingest_payload["tables"][0]["rows_loaded"], 2);
|
|
|
|
|
|
|
|
|
|
|
|
let feature_snapshot = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("snapshot")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature-ingest")
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(feature_snapshot["branch"], "feature-ingest");
|
|
|
|
|
|
|
|
|
|
|
|
let zoe = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature-ingest")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Zoe"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(zoe["row_count"], 1);
|
|
|
|
|
|
assert_eq!(zoe["rows"][0]["p.name"], "Zoe");
|
|
|
|
|
|
|
|
|
|
|
|
let bob = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature-ingest")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Bob"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(bob["row_count"], 1);
|
|
|
|
|
|
assert_eq!(bob["rows"][0]["p.age"], 26);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_export_round_trips_full_branch_graph() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("create")
|
|
|
|
|
|
.arg("--uri")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--from")
|
|
|
|
|
|
.arg("main")
|
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let feature_data = graph.write_jsonl(
|
2026-04-10 20:49:41 +03:00
|
|
|
|
"system-local-export-feature.jsonl",
|
|
|
|
|
|
r#"{"type":"Person","data":{"name":"Eve","age":29}}
|
|
|
|
|
|
{"edge":"Knows","from":"Alice","to":"Eve"}"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("load")
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&feature_data)
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature")
|
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
|
.arg("append")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path()),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let exported = stdout_string(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("export")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("feature")
|
|
|
|
|
|
.arg("--jsonl"),
|
|
|
|
|
|
));
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let export_path = graph.write_jsonl("system-local-exported.jsonl", &exported);
|
|
|
|
|
|
let imported_graph = graph.path().parent().unwrap().join("imported-export.omni");
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("init")
|
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
|
.arg(fixture("test.pg"))
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&imported_graph),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
);
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("load")
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&export_path)
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&imported_graph),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
2026-05-24 16:46:00 +01:00
|
|
|
|
snapshot_table_row_count_at(&imported_graph, "node:Person"),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
5
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
2026-05-24 16:46:00 +01:00
|
|
|
|
snapshot_table_row_count_at(&imported_graph, "node:Company"),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
2
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
2026-05-24 16:46:00 +01:00
|
|
|
|
snapshot_table_row_count_at(&imported_graph, "edge:Knows"),
|
|
|
|
|
|
4
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
snapshot_table_row_count_at(&imported_graph, "edge:WorksAt"),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
2
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let eve = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&imported_graph)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Eve"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(eve["row_count"], 1);
|
|
|
|
|
|
assert_eq!(eve["rows"][0]["p.name"], "Eve");
|
|
|
|
|
|
|
|
|
|
|
|
let friends = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&imported_graph)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("friends_of")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(friends["row_count"], 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_s3_end_to_end_init_load_read_flow() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let Some(graph_uri) = s3_test_graph_uri("cli-local") else {
|
2026-04-10 20:49:41 +03:00
|
|
|
|
eprintln!("skipping s3 cli test: OMNIGRAPH_S3_TEST_BUCKET is not set");
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let temp = tempfile::tempdir().unwrap();
|
|
|
|
|
|
let query_root = temp.path();
|
|
|
|
|
|
let config = query_root.join("omnigraph.yaml");
|
|
|
|
|
|
let query = query_root.join("test.gq");
|
|
|
|
|
|
fs::copy(fixture("test.gq"), &query).unwrap();
|
|
|
|
|
|
write_config(
|
|
|
|
|
|
&config,
|
|
|
|
|
|
&format!(
|
|
|
|
|
|
"\
|
2026-04-14 04:12:14 +03:00
|
|
|
|
graphs:
|
2026-04-10 20:49:41 +03:00
|
|
|
|
rustfs:
|
|
|
|
|
|
uri: '{}'
|
|
|
|
|
|
cli:
|
2026-04-14 04:12:14 +03:00
|
|
|
|
graph: rustfs
|
2026-04-10 20:49:41 +03:00
|
|
|
|
branch: main
|
|
|
|
|
|
query:
|
|
|
|
|
|
roots:
|
|
|
|
|
|
- .
|
|
|
|
|
|
policy: {{}}
|
|
|
|
|
|
",
|
2026-05-24 16:46:00 +01:00
|
|
|
|
graph_uri
|
2026-04-10 20:49:41 +03:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("init")
|
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
|
.arg(fixture("test.pg"))
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&graph_uri),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
);
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("load")
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(fixture("test.jsonl"))
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&graph_uri),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let read = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.current_dir(query_root)
|
|
|
|
|
|
.arg("read")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg("test.gq")
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(read["row_count"], 1);
|
|
|
|
|
|
assert_eq!(read["rows"][0]["p.name"], "Alice");
|
|
|
|
|
|
|
|
|
|
|
|
let snapshot = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.current_dir(query_root)
|
|
|
|
|
|
.arg("snapshot")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert!(snapshot["tables"].is_array());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_failed_load_keeps_target_state_unchanged() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let bad_data = graph.write_jsonl(
|
2026-04-10 20:49:41 +03:00
|
|
|
|
"system-bad-load.jsonl",
|
|
|
|
|
|
r#"{"edge":"Knows","from":"Alice","to":"Missing"}"#,
|
|
|
|
|
|
);
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let person_rows_before = snapshot_table_row_count(&graph, "node:Person");
|
|
|
|
|
|
let knows_rows_before = snapshot_table_row_count(&graph, "edge:Knows");
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("load")
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&bad_data)
|
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
|
.arg("append")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path()),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
|
assert!(stderr.contains("not found") || stderr.contains("Missing"));
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
2026-05-24 16:46:00 +01:00
|
|
|
|
snapshot_table_row_count(&graph, "node:Person"),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
person_rows_before
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
2026-05-24 16:46:00 +01:00
|
|
|
|
snapshot_table_row_count(&graph, "edge:Knows"),
|
2026-04-10 20:49:41 +03:00
|
|
|
|
knows_rows_before
|
|
|
|
|
|
);
|
2026-05-01 22:45:38 +02:00
|
|
|
|
// Failed loads leave no run record (the run lifecycle has been
|
|
|
|
|
|
// removed); atomicity is verified above by the unchanged target.
|
2026-04-10 20:49:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_failed_change_keeps_target_state_unchanged() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let mutation_file = add_friend_query(&graph, "system-invalid-change.gq");
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"from":"Alice","to":"Missing"}"#),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
|
assert!(stderr.contains("not found") || stderr.contains("Missing"));
|
|
|
|
|
|
|
|
|
|
|
|
let friends_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("friends_of")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(friends_payload["row_count"], 2);
|
2026-05-01 22:45:38 +02:00
|
|
|
|
// Failed mutations leave no run record (the run lifecycle has been
|
|
|
|
|
|
// removed); atomicity is verified above by the unchanged target.
|
2026-04-10 20:49:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_resolves_relative_query_against_config_base_dir() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let root = graph.path().parent().unwrap();
|
2026-04-10 20:49:41 +03:00
|
|
|
|
let config_dir = root.join("config");
|
|
|
|
|
|
let query_dir = config_dir.join("queries");
|
|
|
|
|
|
let ambient_dir = root.join("ambient");
|
|
|
|
|
|
fs::create_dir_all(&query_dir).unwrap();
|
|
|
|
|
|
fs::create_dir_all(&ambient_dir).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let config = config_dir.join("omnigraph.yaml");
|
|
|
|
|
|
write_config(
|
|
|
|
|
|
&config,
|
|
|
|
|
|
&format!(
|
|
|
|
|
|
"\
|
2026-04-14 04:12:14 +03:00
|
|
|
|
graphs:
|
2026-04-10 20:49:41 +03:00
|
|
|
|
local:
|
|
|
|
|
|
uri: '{}'
|
|
|
|
|
|
cli:
|
2026-04-14 04:12:14 +03:00
|
|
|
|
graph: local
|
2026-04-10 20:49:41 +03:00
|
|
|
|
branch: main
|
|
|
|
|
|
query:
|
|
|
|
|
|
roots:
|
|
|
|
|
|
- queries
|
|
|
|
|
|
policy: {{}}
|
|
|
|
|
|
",
|
2026-05-24 16:46:00 +01:00
|
|
|
|
graph.path().display()
|
2026-04-10 20:49:41 +03:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
write_query_file(
|
|
|
|
|
|
&query_dir.join("local.gq"),
|
|
|
|
|
|
r#"
|
|
|
|
|
|
query get_person($name: String) {
|
|
|
|
|
|
match {
|
|
|
|
|
|
$p: Person { name: $name }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { $p.age, $p.name }
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
write_query_file(
|
|
|
|
|
|
&ambient_dir.join("local.gq"),
|
|
|
|
|
|
r#"
|
|
|
|
|
|
query get_person($name: String) {
|
|
|
|
|
|
match {
|
|
|
|
|
|
$p: Person { name: $name }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { $p.name }
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.current_dir(&ambient_dir)
|
|
|
|
|
|
.arg("read")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg("local.gq")
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
let columns = payload["columns"]
|
|
|
|
|
|
.as_array()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|value| value.as_str().unwrap())
|
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
assert_eq!(columns, vec!["p.age", "p.name"]);
|
|
|
|
|
|
assert_eq!(payload["rows"][0]["p.age"], 30);
|
|
|
|
|
|
assert_eq!(payload["rows"][0]["p.name"], "Alice");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_datetime_and_list_types_round_trip_through_load_read_and_change() {
|
|
|
|
|
|
let temp = tempfile::tempdir().unwrap();
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = graph_path(temp.path());
|
2026-04-10 20:49:41 +03:00
|
|
|
|
let schema = temp.path().join("datatypes.pg");
|
|
|
|
|
|
let data = temp.path().join("datatypes.jsonl");
|
|
|
|
|
|
let queries = temp.path().join("datatypes.gq");
|
|
|
|
|
|
|
|
|
|
|
|
write_query_file(
|
|
|
|
|
|
&schema,
|
|
|
|
|
|
r#"
|
|
|
|
|
|
node Task {
|
|
|
|
|
|
slug: String @key
|
|
|
|
|
|
title: String
|
|
|
|
|
|
due_at: DateTime
|
|
|
|
|
|
tags: [String]
|
|
|
|
|
|
scores: [I32]?
|
|
|
|
|
|
active_days: [Date]?
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
write_jsonl(
|
|
|
|
|
|
&data,
|
|
|
|
|
|
r#"{"type":"Task","data":{"slug":"alpha","title":"Launch prep","due_at":"2026-04-01T08:30:00Z","tags":["launch","priority"],"scores":[1,2],"active_days":["2026-03-30","2026-03-31"]}}
|
|
|
|
|
|
{"type":"Task","data":{"slug":"beta","title":"Archive","due_at":"2026-05-01T12:00:00Z","tags":["backlog"],"scores":[5],"active_days":["2026-04-01"]}}"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
write_query_file(
|
|
|
|
|
|
&queries,
|
|
|
|
|
|
r#"
|
|
|
|
|
|
query due_with_tag($deadline: DateTime, $tag: String) {
|
|
|
|
|
|
match {
|
|
|
|
|
|
$t: Task
|
|
|
|
|
|
$t.due_at <= $deadline
|
|
|
|
|
|
$t.tags contains $tag
|
|
|
|
|
|
}
|
|
|
|
|
|
return { $t.slug, $t.due_at, $t.tags, $t.scores, $t.active_days }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
query insert_task(
|
|
|
|
|
|
$slug: String,
|
|
|
|
|
|
$title: String,
|
|
|
|
|
|
$due_at: DateTime,
|
|
|
|
|
|
$tags: [String],
|
|
|
|
|
|
$scores: [I32],
|
|
|
|
|
|
$active_days: [Date]
|
|
|
|
|
|
) {
|
|
|
|
|
|
insert Task {
|
|
|
|
|
|
slug: $slug,
|
|
|
|
|
|
title: $title,
|
|
|
|
|
|
due_at: $due_at,
|
|
|
|
|
|
tags: $tags,
|
|
|
|
|
|
scores: $scores,
|
|
|
|
|
|
active_days: $active_days
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
query update_task(
|
|
|
|
|
|
$slug: String,
|
|
|
|
|
|
$due_at: DateTime,
|
|
|
|
|
|
$tags: [String],
|
|
|
|
|
|
$scores: [I32],
|
|
|
|
|
|
$active_days: [Date]
|
|
|
|
|
|
) {
|
|
|
|
|
|
update Task set {
|
|
|
|
|
|
due_at: $due_at,
|
|
|
|
|
|
tags: $tags,
|
|
|
|
|
|
scores: $scores,
|
|
|
|
|
|
active_days: $active_days
|
|
|
|
|
|
} where slug = $slug
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
query get_task($slug: String) {
|
|
|
|
|
|
match { $t: Task { slug: $slug } }
|
|
|
|
|
|
return { $t.slug, $t.due_at, $t.tags, $t.scores, $t.active_days }
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
output_success(cli().arg("init").arg("--schema").arg(&schema).arg(&graph));
|
|
|
|
|
|
output_success(cli().arg("load").arg("--data").arg(&data).arg(&graph));
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
let filtered = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&graph)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&queries)
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("due_with_tag")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"deadline":"2026-04-02T00:00:00Z","tag":"launch"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(filtered["row_count"], 1);
|
|
|
|
|
|
assert_eq!(filtered["rows"][0]["t.slug"], "alpha");
|
|
|
|
|
|
assert_eq!(filtered["rows"][0]["t.due_at"], "2026-04-01T08:30:00.000Z");
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
filtered["rows"][0]["t.tags"],
|
|
|
|
|
|
serde_json::json!(["launch", "priority"])
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(filtered["rows"][0]["t.scores"], serde_json::json!([1, 2]));
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
filtered["rows"][0]["t.active_days"],
|
|
|
|
|
|
serde_json::json!(["2026-03-30", "2026-03-31"])
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let insert_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&graph)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&queries)
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("insert_task")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(
|
|
|
|
|
|
r#"{"slug":"gamma","title":"Embed prep","due_at":"2026-04-03T09:15:00Z","tags":["embed","launch"],"scores":[3,8],"active_days":["2026-04-02","2026-04-03"]}"#,
|
|
|
|
|
|
)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(insert_payload["affected_nodes"], 1);
|
|
|
|
|
|
|
|
|
|
|
|
let update_payload = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&graph)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&queries)
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("update_task")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"slug":"gamma","due_at":"2026-04-04T10:45:00Z","tags":["embed","released"],"scores":[13,21],"active_days":["2026-04-04","2026-04-05"]}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(update_payload["affected_nodes"], 1);
|
|
|
|
|
|
|
|
|
|
|
|
let gamma = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&graph)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&queries)
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_task")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"slug":"gamma"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(gamma["row_count"], 1);
|
|
|
|
|
|
assert_eq!(gamma["rows"][0]["t.slug"], "gamma");
|
|
|
|
|
|
assert_eq!(gamma["rows"][0]["t.due_at"], "2026-04-04T10:45:00.000Z");
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
gamma["rows"][0]["t.tags"],
|
|
|
|
|
|
serde_json::json!(["embed", "released"])
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(gamma["rows"][0]["t.scores"], serde_json::json!([13, 21]));
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
gamma["rows"][0]["t.active_days"],
|
|
|
|
|
|
serde_json::json!(["2026-04-04", "2026-04-05"])
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
#[ignore = "requires GEMINI_API_KEY and network access"]
|
|
|
|
|
|
fn local_cli_real_gemini_string_nearest_query_returns_expected_match() {
|
|
|
|
|
|
let temp = tempfile::tempdir().unwrap();
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = graph_path(temp.path());
|
2026-04-10 20:49:41 +03:00
|
|
|
|
let schema = temp.path().join("gemini.pg");
|
|
|
|
|
|
let data = temp.path().join("gemini.jsonl");
|
|
|
|
|
|
let queries = temp.path().join("gemini.gq");
|
|
|
|
|
|
|
|
|
|
|
|
write_query_file(
|
|
|
|
|
|
&schema,
|
|
|
|
|
|
r#"
|
|
|
|
|
|
node Doc {
|
|
|
|
|
|
slug: String @key
|
|
|
|
|
|
title: String
|
|
|
|
|
|
embedding: Vector(4) @index
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let alpha = embed_text_with_gemini("alpha", 4);
|
|
|
|
|
|
let beta = embed_text_with_gemini("beta", 4);
|
|
|
|
|
|
let gamma = embed_text_with_gemini("gamma", 4);
|
|
|
|
|
|
write_jsonl(
|
|
|
|
|
|
&data,
|
|
|
|
|
|
&format!(
|
|
|
|
|
|
r#"{{"type":"Doc","data":{{"slug":"alpha-doc","title":"alpha","embedding":[{}]}}}}
|
|
|
|
|
|
{{"type":"Doc","data":{{"slug":"beta-doc","title":"beta","embedding":[{}]}}}}
|
|
|
|
|
|
{{"type":"Doc","data":{{"slug":"gamma-doc","title":"gamma","embedding":[{}]}}}}"#,
|
|
|
|
|
|
format_vector(&alpha),
|
|
|
|
|
|
format_vector(&beta),
|
|
|
|
|
|
format_vector(&gamma),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
write_query_file(
|
|
|
|
|
|
&queries,
|
|
|
|
|
|
r#"
|
|
|
|
|
|
query vector_search($q: String) {
|
|
|
|
|
|
match { $d: Doc }
|
|
|
|
|
|
return { $d.slug, $d.title }
|
|
|
|
|
|
order { nearest($d.embedding, $q) }
|
|
|
|
|
|
limit 3
|
|
|
|
|
|
}
|
|
|
|
|
|
"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
output_success(cli().arg("init").arg("--schema").arg(&schema).arg(&graph));
|
|
|
|
|
|
output_success(cli().arg("load").arg("--data").arg(&data).arg(&graph));
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
let result = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(&graph)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&queries)
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("vector_search")
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"q":"alpha"}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(result["row_count"], 3);
|
|
|
|
|
|
assert_eq!(result["rows"][0]["d.slug"], "alpha-doc");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 22:45:38 +02:00
|
|
|
|
// The publisher CAS conflict shape is verified end-to-end at the engine
|
|
|
|
|
|
// level in
|
|
|
|
|
|
// `crates/omnigraph/tests/runs.rs::concurrent_writers_one_succeeds_one_gets_expected_version_mismatch`
|
2026-04-30 08:52:50 +02:00
|
|
|
|
// and at the HTTP boundary in
|
|
|
|
|
|
// `crates/omnigraph-server/tests/server.rs::change_conflict_returns_manifest_conflict_409`.
|
2026-05-01 22:45:38 +02:00
|
|
|
|
// A CLI-level race would be timing-dependent; with direct-publish the
|
|
|
|
|
|
// surface is the same engine path the unit test already covers.
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
#[test]
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
fn local_cli_policy_tooling_is_end_to_end() {
|
|
|
|
|
|
// Sanity check for the read-only policy CLI surfaces. These don't
|
|
|
|
|
|
// mutate the graph — they just parse and evaluate the policy file —
|
|
|
|
|
|
// so they don't depend on PR #4's engine-side enforcement.
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
|
|
|
|
|
graph.write_config("policy.tests.yaml", POLICY_E2E_TESTS_YAML);
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
|
|
|
|
|
let validate = output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("policy")
|
|
|
|
|
|
.arg("validate")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config),
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(stdout_string(&validate).contains("policy valid:"));
|
|
|
|
|
|
|
|
|
|
|
|
let tests = output_success(cli().arg("policy").arg("test").arg("--config").arg(&config));
|
|
|
|
|
|
assert!(stdout_string(&tests).contains("policy tests passed: 2 cases"));
|
|
|
|
|
|
|
|
|
|
|
|
let explain = output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("policy")
|
|
|
|
|
|
.arg("explain")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--actor")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("--action")
|
|
|
|
|
|
.arg("change")
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("main"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let explain_stdout = stdout_string(&explain);
|
|
|
|
|
|
assert!(explain_stdout.contains("decision: deny"));
|
|
|
|
|
|
assert!(explain_stdout.contains("branch: main"));
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_change_enforces_engine_layer_policy() {
|
|
|
|
|
|
// Asserts MR-722 PR #4: when `policy.file` is configured in
|
|
|
|
|
|
// `omnigraph.yaml`, the CLI loads PolicyEngine into Omnigraph and
|
|
|
|
|
|
// every direct-engine write hits `enforce(action, scope, actor)` —
|
|
|
|
|
|
// identical to what the HTTP server gets, regardless of transport.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Three cases, each discriminating:
|
|
|
|
|
|
//
|
|
|
|
|
|
// 1. Policy installed, no actor source (no `cli.actor` in config,
|
|
|
|
|
|
// no `--as` flag) → engine-layer footgun guard fires; CLI exits
|
|
|
|
|
|
// non-zero with a "no actor" message. Silent bypass is the bug
|
|
|
|
|
|
// PR #4 prevents.
|
|
|
|
|
|
// 2. Policy installed, `--as act-bruno`, change on main → Cedar
|
|
|
|
|
|
// denies (bruno can change unprotected branches; main is
|
|
|
|
|
|
// protected). CLI exits non-zero with a "denied" message.
|
|
|
|
|
|
// 3. Policy installed, `--as act-ragnor`, change on main →
|
|
|
|
|
|
// Cedar permits (admins-write rule). Write succeeds and the
|
|
|
|
|
|
// inserted row is readable.
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
|
|
|
|
|
let mutation_file = insert_person_query(&graph, "system-local-policy-change.gq");
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
|
|
|
|
|
|
// Case 1: policy configured, no actor threaded → footgun guard.
|
|
|
|
|
|
let no_actor = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"NoActorPerson","age":1}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let no_actor_stderr = String::from_utf8_lossy(&no_actor.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
no_actor_stderr.contains("no actor"),
|
|
|
|
|
|
"expected 'no actor' footgun message, got stderr: {no_actor_stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Case 2: `--as act-bruno` against protected main → denied.
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("change")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"BrunoOnMain","age":2}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let denied_stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
denied_stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' message for bruno/main, got stderr: {denied_stderr}"
|
|
|
|
|
|
);
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
// Case 3: `--as act-ragnor` against main → permitted by admins-write.
|
|
|
|
|
|
let allowed = parse_stdout_json(&output_success(
|
2026-04-10 20:49:41 +03:00
|
|
|
|
cli()
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("change")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--params")
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
.arg(r#"{"name":"RagnorOnMain","age":3}"#)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
assert_eq!(allowed["branch"], "main");
|
|
|
|
|
|
assert_eq!(allowed["affected_nodes"], 1);
|
|
|
|
|
|
assert_eq!(allowed["actor_id"], "act-ragnor");
|
2026-04-10 20:49:41 +03:00
|
|
|
|
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
// Verify the row landed — proves the write actually committed, not
|
|
|
|
|
|
// just that enforce returned Ok and silently dropped the work.
|
2026-04-10 20:49:41 +03:00
|
|
|
|
let verify = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("read")
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.arg(graph.path())
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
|
.arg("--name")
|
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
|
.arg("--params")
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
.arg(r#"{"name":"RagnorOnMain"}"#)
|
2026-04-10 20:49:41 +03:00
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(verify["row_count"], 1);
|
policy: CLI policy injection — local writes go through engine enforce (MR-722) (#104)
Closes the CLI side of the policy chassis fan-out. Before this commit,
CLI direct-engine writes bypassed Cedar entirely because the CLI never
called `Omnigraph::with_policy(...)` for non-`policy validate|test|explain`
subcommands. After this commit, every CLI direct-engine writer
(change, load, ingest, branch create/delete/merge, schema apply) opens
the engine via a new `open_local_db_with_policy(uri, &config)` helper
that installs the configured `PolicyEngine` when `policy.file` is set,
and threads the resolved actor through to the `_as` writer methods.
Actor identity resolution:
- New top-level `--as <ACTOR>` global flag on the CLI overrides config.
- New `cli.actor` field in `omnigraph.yaml` provides a default actor.
- Precedence: `--as` > `cli.actor` > None.
- When policy is configured and neither is set, the engine-layer
footgun guard fires and the write is denied — silent bypass via
"I forgot the actor" is exactly what the guard prevents.
- Remote HTTP writes ignore both — bearer-token-resolved server-side.
Helpers added in main.rs:
- `open_local_db_with_policy(uri, &config) -> Result<Omnigraph>` —
opens the DB and installs the PolicyEngine when configured. Without
policy this is identical to a bare `Omnigraph::open`.
- `resolve_cli_actor(cli_as, &config) -> Option<&str>` — implements
the flag > config > None precedence.
Engine: added `load_file_as` to the loader as the actor-aware mirror of
`load_file`, so CLI file-path loads flow through the same enforce gate
as in-memory `load_as` calls.
Test rewrite: `local_cli_policy_tooling_is_end_to_end_while_local_writes_stay_unenforced`
was the explicit assertion of the pre-chassis hole. Renamed and split:
- `local_cli_policy_tooling_is_end_to_end` — sanity for the read-only
policy CLI surfaces (validate/test/explain), unchanged behavior.
- `local_cli_change_enforces_engine_layer_policy` — the new assertion:
policy installed + no actor → footgun-guard denial; `--as act-bruno`
on protected main → Cedar denial; `--as act-ragnor` (admins-write
rule) on main → permit, write committed.
POLICY_E2E_YAML gains an `admins-write` rule so the permit case has
a non-trivial actor to exercise.
docs/user/policy.md updated with `cli.actor` + `--as <ACTOR>` usage.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:21 +03:00
|
|
|
|
assert_eq!(verify["rows"][0]["p.name"], "RagnorOnMain");
|
2026-04-10 20:49:41 +03:00
|
|
|
|
}
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
|
|
|
|
|
|
// ─── MR-722 PR A: CLI×writer matrix ───────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// The change writer is covered above by `local_cli_change_enforces_engine_layer_policy`.
|
|
|
|
|
|
// These tests extend the engine-layer-policy assertion to the other 6
|
|
|
|
|
|
// writers, asserting each `omnigraph <writer> --as <actor>` invocation
|
|
|
|
|
|
// reaches the corresponding `_as` method and Cedar evaluates correctly.
|
|
|
|
|
|
// One denied case (`--as act-bruno`) + one allowed case (`--as act-ragnor`
|
|
|
|
|
|
// via the `admins-*` rules) per writer; the no-actor footgun is already
|
|
|
|
|
|
// proved by the change-writer test and applies identically to every
|
|
|
|
|
|
// other `_as` variant.
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_load_enforces_engine_layer_policy() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
|
|
|
|
|
let data = graph.write_jsonl(
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
"system-local-policy-load.jsonl",
|
|
|
|
|
|
r#"{"type":"Person","data":{"name":"LoadPolicy","age":11}}"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// act-bruno: change-on-protected is denied (team-write-unprotected only).
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("load")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&data)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' for bruno/main load, got: {stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// act-ragnor: admins-write rule permits change anywhere.
|
|
|
|
|
|
let allowed = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("load")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&data)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(allowed["branch"], "main");
|
|
|
|
|
|
assert!(allowed["nodes_loaded"].as_u64().unwrap() >= 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_ingest_enforces_engine_layer_policy() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
|
|
|
|
|
let data = graph.write_jsonl(
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
"system-local-policy-ingest.jsonl",
|
|
|
|
|
|
r#"{"type":"Person","data":{"name":"IngestPolicy","age":12}}"#,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// act-bruno: ingest into a new branch requires both BranchCreate and
|
|
|
|
|
|
// Change. Bruno has change-unprotected only, and the implicit
|
|
|
|
|
|
// branch_create fires first when the target branch doesn't exist.
|
|
|
|
|
|
// Either gate is enough to deny — assert denial without pinning
|
|
|
|
|
|
// which one fires first.
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("ingest")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&data)
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("policy-ingest-feature")
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' for bruno ingest, got: {stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// act-ragnor: admins-write covers Change, admins-branch-ops covers
|
|
|
|
|
|
// BranchCreate. Both fire as ingest creates the branch + loads.
|
|
|
|
|
|
let allowed = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("ingest")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--data")
|
|
|
|
|
|
.arg(&data)
|
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
|
.arg("policy-ingest-feature")
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(allowed["branch"], "policy-ingest-feature");
|
|
|
|
|
|
assert_eq!(allowed["branch_created"], true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_schema_apply_enforces_engine_layer_policy() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
|
|
|
|
|
|
// Additive: add a nullable property; SDK-compatible with the fixture
|
|
|
|
|
|
// schema. Uses the schema-apply scope (TargetBranch("main")).
|
|
|
|
|
|
let new_schema = std::fs::read_to_string(fixture("test.pg"))
|
|
|
|
|
|
.unwrap()
|
2026-05-24 16:46:00 +01:00
|
|
|
|
.replace(
|
|
|
|
|
|
" age: I32?\n}",
|
|
|
|
|
|
" age: I32?\n nickname: String?\n}",
|
|
|
|
|
|
);
|
|
|
|
|
|
let schema_path = graph.path().join("policy-additive.pg");
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
std::fs::write(&schema_path, &new_schema).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("schema")
|
|
|
|
|
|
.arg("apply")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' for bruno schema apply, got: {stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let allowed = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("schema")
|
|
|
|
|
|
.arg("apply")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(allowed["applied"], true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_branch_create_enforces_engine_layer_policy() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("create")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--from")
|
|
|
|
|
|
.arg("main")
|
|
|
|
|
|
.arg("bruno-feature"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' for bruno branch create, got: {stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("create")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--from")
|
|
|
|
|
|
.arg("main")
|
|
|
|
|
|
.arg("ragnor-feature"),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_branch_delete_enforces_engine_layer_policy() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
|
|
|
|
|
|
// Pre-create the branch as ragnor so there's something to delete.
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("create")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--from")
|
|
|
|
|
|
.arg("main")
|
|
|
|
|
|
.arg("doomed"),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("delete")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("doomed"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' for bruno branch delete, got: {stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("delete")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("doomed"),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_branch_merge_enforces_engine_layer_policy() {
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config("omnigraph-policy.yaml", &local_policy_config(&graph));
|
|
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
|
|
|
|
|
|
// Pre-create a feature branch as ragnor (admins-branch-ops covers it).
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("create")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--from")
|
|
|
|
|
|
.arg("main")
|
|
|
|
|
|
.arg("merge-feature"),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("merge")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("merge-feature")
|
|
|
|
|
|
.arg("--into")
|
|
|
|
|
|
.arg("main"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' for bruno branch merge, got: {stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-ragnor")
|
|
|
|
|
|
.arg("branch")
|
|
|
|
|
|
.arg("merge")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("merge-feature")
|
|
|
|
|
|
.arg("--into")
|
|
|
|
|
|
.arg("main"),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── MR-722 PR A: cli.actor config-only precedence ────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// The change-writer test above uses `--as` directly. These two tests
|
|
|
|
|
|
// pin the precedence rule that `main.rs::resolve_cli_actor` implements:
|
|
|
|
|
|
// `--as` flag > `cli.actor` from `omnigraph.yaml` > None.
|
|
|
|
|
|
|
2026-05-24 16:46:00 +01:00
|
|
|
|
fn local_policy_config_with_actor(graph: &SystemGraph, actor: &str) -> String {
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
// Mirrors `local_policy_config` but adds `cli.actor` so the
|
|
|
|
|
|
// config-only precedence path is exercised. The `cli:` block
|
|
|
|
|
|
// already has `graph` and `branch`; appending `actor` here.
|
|
|
|
|
|
format!(
|
|
|
|
|
|
"\
|
|
|
|
|
|
project:
|
|
|
|
|
|
name: policy-e2e-local
|
|
|
|
|
|
graphs:
|
|
|
|
|
|
local:
|
|
|
|
|
|
uri: {}
|
|
|
|
|
|
cli:
|
|
|
|
|
|
graph: local
|
|
|
|
|
|
branch: main
|
|
|
|
|
|
actor: {}
|
|
|
|
|
|
query:
|
|
|
|
|
|
roots:
|
|
|
|
|
|
- .
|
|
|
|
|
|
policy:
|
|
|
|
|
|
file: ./policy.yaml
|
|
|
|
|
|
",
|
2026-05-24 16:46:00 +01:00
|
|
|
|
yaml_string(&graph.path().to_string_lossy()),
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
actor,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_actor_from_config_used_when_no_flag() {
|
|
|
|
|
|
// cli.actor: act-ragnor in omnigraph.yaml, no --as flag → change
|
|
|
|
|
|
// permitted via admins-write rule. Proves the config-only path
|
|
|
|
|
|
// works; previously the only proof was structural.
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config(
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
"omnigraph-policy.yaml",
|
2026-05-24 16:46:00 +01:00
|
|
|
|
&local_policy_config_with_actor(&graph, "act-ragnor"),
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
);
|
2026-05-24 16:46:00 +01:00
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
|
|
|
|
|
let mutation_file = insert_person_query(&graph, "system-local-cli-actor.gq");
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
|
|
|
|
|
|
let allowed = parse_stdout_json(&output_success(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("change")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"ConfigActorEve","age":18}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(allowed["affected_nodes"], 1);
|
|
|
|
|
|
assert_eq!(allowed["actor_id"], "act-ragnor");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn local_cli_actor_flag_overrides_config_actor() {
|
|
|
|
|
|
// cli.actor: act-ragnor in config + --as act-bruno on CLI → change
|
|
|
|
|
|
// denied. Flag wins per the precedence rule. Without this test, a
|
|
|
|
|
|
// future change that reverses precedence would ride through silently.
|
2026-05-24 16:46:00 +01:00
|
|
|
|
let graph = SystemGraph::loaded();
|
|
|
|
|
|
let config = graph.write_config(
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
"omnigraph-policy.yaml",
|
2026-05-24 16:46:00 +01:00
|
|
|
|
&local_policy_config_with_actor(&graph, "act-ragnor"),
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
);
|
2026-05-24 16:46:00 +01:00
|
|
|
|
graph.write_config("policy.yaml", POLICY_E2E_YAML);
|
|
|
|
|
|
let mutation_file = insert_person_query(&graph, "system-local-cli-actor-override.gq");
|
tests: policy chassis e2e gap-fills (MR-722 follow-up) (#106)
* tests: policy chassis e2e gap-fills (MR-722 follow-up)
Audit after PRs #101-105 surfaced real e2e gaps in the policy chassis
that could let regressions ride through silently. Coverage was strong
at the SDK level (18 chassis tests) and reasonable at HTTP (12+ policy
tests), but the CLI×writer matrix was asymmetric (only `change` tested
end-to-end), the `cli.actor` config-only precedence path was untested,
the `OMNIGRAPH_UNAUTHENTICATED` env-var read path was unexercised,
`serve()`'s startup-refusal propagation was structural-review only,
and engine↔HTTP decision parity was a structural property without a
test pinning it. This commit closes those gaps.
Added (15 new tests, all test-only):
* `policy_engine_chassis.rs` (+2): `load_file_as` allow + deny pair —
PR #104 added the actor-aware mirror of `load_file` but it was only
exercised via CLI integration; this is direct-SDK coverage.
* `omnigraph-server/src/lib.rs` mod tests (+2):
- `unauthenticated_env_var_classification` — consolidated single
test (process-global env var; running parallel would race) that
pins truthy values, falsy values, unset, and CLI-flag-overrides-
env behavior of the `OMNIGRAPH_UNAUTHENTICATED` read path inside
`load_server_settings`.
- `serve_refuses_to_start_in_state_1_without_unauthenticated` —
`#[serial]` integration test. Clears all bearer-token env vars,
builds a `ServerConfig` with no policy file and no flag, calls
`serve(config).await`, asserts Err before any side-effecting
work (Lance dataset open, TcpListener::bind). Guards the
classifier→serve propagation path so a future refactor that
drops the call turns red.
* `omnigraph-server/tests/server.rs` (+4): `policy_decision_parity_*`
— four cases (Change×allowed+denied, BranchMerge×allowed+denied).
Each case runs the same Cedar decision via both SDK
(`Omnigraph::with_policy().mutate_as` / `branch_merge_as`) and HTTP
(`POST /change` / `POST /branches/merge`) and asserts both either
Allow or Deny. The structural property (both paths call
`PolicyChecker::check`) is now test-asserted.
* `omnigraph-cli/tests/system_local.rs` (+8): the CLI×writer matrix
fan-out:
- `local_cli_load_enforces_engine_layer_policy`
- `local_cli_ingest_enforces_engine_layer_policy`
- `local_cli_schema_apply_enforces_engine_layer_policy`
- `local_cli_branch_create_enforces_engine_layer_policy`
- `local_cli_branch_delete_enforces_engine_layer_policy`
- `local_cli_branch_merge_enforces_engine_layer_policy`
Each: one denied case (`--as act-bruno` against protected main) +
one allowed case (`--as act-ragnor` via existing/extended admins-*
rules).
Plus:
- `local_cli_actor_from_config_used_when_no_flag` — proves the
config-only precedence path works.
- `local_cli_actor_flag_overrides_config_actor` — proves the
`--as` flag wins over `cli.actor` in the config.
Adds `local_policy_config_with_actor` helper. Extends
`POLICY_E2E_YAML` with `admins-branch-ops` (BranchCreate +
BranchDelete) and `admins-schema-apply` rules so the CLI×writer
matrix has positive-case rule coverage.
Verification: all new tests pass; full `cargo test --workspace
--locked` is green; `scripts/check-agents-md.sh` passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* tests: serialize env-touching server lib tests to fix CI flake
CI flake on PR #106's Test Workspace job: two of the new tests
(`serve_refuses_to_start_in_state_1_without_unauthenticated` and
`unauthenticated_env_var_classification`) raced against
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`,
which sets `OMNIGRAPH_SERVER_BEARER_TOKEN` via `EnvGuard`.
While `serve_refuses` was mid-execution with its EnvGuard cleared,
the bearer-token test's EnvGuard had `OMNIGRAPH_SERVER_BEARER_TOKEN`
set; `resolve_token_source()` saw it and classified the runtime
state as `DefaultDeny` rather than refusing — so the test panicked
with "Dataset at path X not found" instead of the expected refusal
message. The unauthenticated test had the symmetric failure: its
`OMNIGRAPH_UNAUTHENTICATED="anything"` got overwritten by a peer
`EnvGuard` drop.
Fix: mark every test that uses `EnvGuard` with `#[serial]` so they
serialize against each other (default key). Already on
`serve_refuses_to_start_in_state_1_without_unauthenticated`; added
to `unauthenticated_env_var_classification` and
`server_bearer_tokens_from_env_reads_legacy_token_and_token_file`.
The `parse_bearer_tokens_json_*` tests don't touch env vars and
stay parallel.
Locally green (36 tests pass on my workstation); the parallelism
issue is CI-runner-specific (more aggressive thread interleaving)
but the fix is universal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:25:04 +03:00
|
|
|
|
|
|
|
|
|
|
let denied = output_failure(
|
|
|
|
|
|
cli()
|
|
|
|
|
|
.arg("--as")
|
|
|
|
|
|
.arg("act-bruno")
|
|
|
|
|
|
.arg("change")
|
|
|
|
|
|
.arg("--config")
|
|
|
|
|
|
.arg(&config)
|
|
|
|
|
|
.arg("--query")
|
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
|
.arg("--params")
|
|
|
|
|
|
.arg(r#"{"name":"OverrideEve","age":19}"#)
|
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
|
);
|
|
|
|
|
|
let stderr = String::from_utf8_lossy(&denied.stderr);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stderr.contains("denied"),
|
|
|
|
|
|
"expected 'denied' when --as overrides config to bruno, got: {stderr}"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|