2026-06-11 15:16:51 +03:00
|
|
|
//! Data commands: load/read/change/branch/commit/export/snapshot/policy/embed/maintenance.
|
|
|
|
|
//! Moved verbatim from tests/cli.rs in the modularization.
|
|
|
|
|
|
|
|
|
|
use std::fs;
|
|
|
|
|
|
feat(cli): RFC-011 Slice A — additive scope/profile addressing (#235)
* feat(cli): RFC-011 Slice A — operator-config scope structs (profiles/clusters/defaults)
Additive operator-config surface for the RFC-011 scope model. No behavior
change yet — these structs are parsed but not consumed until the scope
resolver lands.
- OperatorConfig gains `profiles:` (name → OperatorProfile) and `clusters:`
(name → OperatorCluster { root }) — the latter the only place a storage
root appears in operator config (RFC-011 storage-root rule).
- OperatorDefaults gains `server` and `default_graph` (the flat-default scope).
- OperatorProfile binds one of {server, cluster, store} + default_graph;
`binding()` validates exactly-one on use and returns a ScopeBinding.
- Accessors profile()/cluster_root()/default_server()/default_graph();
unknown-key warnings extended to the new blocks (forward-compat preserved —
old configs still load, new keys are no longer "unknown").
Tests: parse profiles/clusters/scope-defaults, binding rejects zero/multiple
entities, unknown keys in a profile warn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): RFC-011 Slice A — scope resolver + --profile/--store, wired (additive)
Translate the new scope inputs into the existing addressing tuple, in front of
the unchanged resolvers. Purely additive: an explicit address
(--uri/--target/--server/--store) passes straight through, so every existing
invocation is byte-for-byte unchanged.
- scope.rs: resolve_scope() with the RFC-011 precedence (explicit > --profile /
OMNIGRAPH_PROFILE > flat defaults.server), producing the effective
(server, graph, uri, target) for data verbs and (cluster, cluster_graph) for
maintenance. Plane×scope capability check (server scope rejected on a
maintenance verb; cluster scope rejected on a data verb; store rejects --graph)
fires only on the new paths. 9 unit tests.
- cli.rs: global --profile <NAME> and --store <URI>. (--graph keeps
requires=server for now; profile/default graph comes from default_graph —
profile+--graph override is deferred to the --cluster-graph rework.)
- client.rs: the two GraphClient factories call resolve_scope (Plane::Data) up
front; the explicit branch reproduces today's behavior exactly.
- main.rs: the 15 data call sites forward --profile/--store; the 3 maintenance
verbs consult the scope (Plane::Storage) only when no explicit per-command
address is given, so cluster-binding profiles and --store reach
optimize/repair/cleanup.
Verified: the full omnigraph-cli suite (221 tests) stays green untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test+docs(cli): RFC-011 Slice A — end-to-end scope test + reference docs
- cli_data.rs: prove --store and a --profile store binding drive a read
identically to the legacy positional URI (the additive-coexistence contract),
end to end against a local graph (no server needed).
- cli/reference.md: document profiles/clusters/defaults.server/default_graph,
the --profile/--store flags, and a "Scopes & profiles" section; note the model
coexists with legacy addressing (nothing removed yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:37:55 +03:00
|
|
|
use assert_cmd::Command;
|
2026-06-11 15:16:51 +03:00
|
|
|
use serde_json::Value;
|
|
|
|
|
use tempfile::tempdir;
|
|
|
|
|
|
|
|
|
|
mod support;
|
|
|
|
|
|
|
|
|
|
use support::*;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn short_version_flag_prints_current_cli_version() {
|
|
|
|
|
let output = output_success(cli().arg("-v"));
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
stdout.trim(),
|
|
|
|
|
format!("omnigraph {}", env!("CARGO_PKG_VERSION"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn long_version_flag_prints_current_cli_version() {
|
|
|
|
|
let output = output_success(cli().arg("--version"));
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
stdout.trim(),
|
|
|
|
|
format!("omnigraph {}", env!("CARGO_PKG_VERSION"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn embed_seed_fills_missing_and_preserves_existing_vectors_by_default() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let seed = write_seed_fixture(temp.path());
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.env("OMNIGRAPH_EMBEDDINGS_MOCK", "1")
|
|
|
|
|
.arg("embed")
|
|
|
|
|
.arg("--seed")
|
|
|
|
|
.arg(&seed)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["mode"], "fill_missing");
|
|
|
|
|
assert_eq!(payload["embedded_rows"], 1);
|
|
|
|
|
assert_eq!(payload["selected_rows"], 2);
|
|
|
|
|
|
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
embedded[0]["data"]["embedding"].as_array().unwrap().len(),
|
|
|
|
|
4
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
embedded[1]["data"]["embedding"],
|
|
|
|
|
serde_json::json!([0.1, 0.2])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn embed_clean_removes_selected_embeddings() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let seed = write_seed_fixture(temp.path());
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("embed")
|
|
|
|
|
.arg("--seed")
|
|
|
|
|
.arg(&seed)
|
|
|
|
|
.arg("--clean")
|
|
|
|
|
.arg("--select")
|
|
|
|
|
.arg("Decision:slug=dec-beta")
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["mode"], "clean");
|
|
|
|
|
assert_eq!(payload["cleaned_rows"], 1);
|
|
|
|
|
|
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
|
|
|
assert!(embedded[0]["data"].get("embedding").is_none());
|
|
|
|
|
assert!(embedded[1]["data"].get("embedding").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn embed_select_reembeds_only_matching_rows() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let seed = write_seed_fixture(temp.path());
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.env("OMNIGRAPH_EMBEDDINGS_MOCK", "1")
|
|
|
|
|
.arg("embed")
|
|
|
|
|
.arg("--seed")
|
|
|
|
|
.arg(&seed)
|
|
|
|
|
.arg("--select")
|
|
|
|
|
.arg("Decision:slug=dec-beta")
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["mode"], "reembed_selected");
|
|
|
|
|
assert_eq!(payload["embedded_rows"], 1);
|
|
|
|
|
assert_eq!(payload["selected_rows"], 1);
|
|
|
|
|
|
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
|
|
|
assert!(embedded[0]["data"].get("embedding").is_none());
|
|
|
|
|
assert_ne!(
|
|
|
|
|
embedded[1]["data"]["embedding"],
|
|
|
|
|
serde_json::json!([0.1, 0.2])
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
embedded[1]["data"]["embedding"].as_array().unwrap().len(),
|
|
|
|
|
4
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn embed_seed_preserves_non_entity_rows() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let seed = write_seed_fixture_with_edge(temp.path());
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.env("OMNIGRAPH_EMBEDDINGS_MOCK", "1")
|
|
|
|
|
.arg("embed")
|
|
|
|
|
.arg("--seed")
|
|
|
|
|
.arg(&seed)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["rows"], 3);
|
|
|
|
|
assert_eq!(payload["embedded_rows"], 1);
|
|
|
|
|
|
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
|
|
|
assert_eq!(embedded.len(), 3);
|
|
|
|
|
assert_eq!(embedded[2]["edge"], "Triggered");
|
|
|
|
|
assert_eq!(embedded[2]["from"], "sig-alpha");
|
|
|
|
|
assert_eq!(embedded[2]["to"], "dec-alpha");
|
|
|
|
|
}
|
|
|
|
|
|
feat(cli): RFC-010 Slice 1 — declared plane capability surface + honest addressing (#217)
* feat(cli): declared plane capability surface + wrong-plane guard (RFC-010 Slice 1)
New `planes.rs` is the single source of truth for which plane each subcommand
belongs to (Data / Storage / Control / Session). `command_plane` is an
exhaustive match — adding a `Command` variant is a compile error until its
plane is declared, so the surface cannot silently drift from the command set.
It descends into the nested enums where the plane differs per subcommand
(`schema plan` is storage while `schema show/apply` are data; `queries
validate` opens the graph while `queries list` reads only config).
`guard_addressing` runs once in `main` before dispatch: the data-plane
addressing flags `--server`/`--graph` on any non-data verb now fail with one
declared, pinned error instead of being silently ignored (`optimize --server
prod` previously dropped `--server`). `init`'s message drops the `--target`
half since it takes only a positional URI today.
Test: `cli_schema_config::schema_plan_with_server_flag_errors_wrong_plane`
pins the per-subcommand label, proving the guard descends into the nested enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): storage-plane verbs fail loudly on a remote target (RFC-010 Slice 1)
`optimize`/`repair`/`cleanup` switch from `resolve_uri` to `resolve_local_uri`,
so a `--target` (or positional URI) that resolves to a remote server now fails
with a declared storage-plane message instead of whatever `Omnigraph::open`
said about an `http(s)://` URI. The `resolve_local_graph` bail is reworded to
that storage-plane message, so every storage verb already on the local resolver
(`schema plan`, `queries validate`, `lint`) speaks with one voice.
Net: `optimize --target knowledge` resolves to the graph's storage URI and runs
embedded; `optimize --target prod` (remote) fails loudly; `optimize --server`
is caught earlier by the guard. Positional-URI invocations are unchanged.
Tests (pinned strings, per RFC-010's test plan): optimize happy path on a local
graph, `optimize --server` wrong-plane error, `optimize <https>` storage-plane
error; the existing `query_lint_rejects_http_targets_without_schema` assertion
is updated to the new shared message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:45:58 +03:00
|
|
|
#[test]
|
|
|
|
|
fn optimize_json_succeeds_on_local_graph() {
|
|
|
|
|
// Happy path for the resolve_local_uri swap (RFC-010 Slice 1): a positional
|
|
|
|
|
// local path still resolves and runs embedded.
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_success(cli().arg("optimize").arg("--json").arg(&graph));
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert!(payload["tables"].as_array().is_some());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn optimize_with_server_flag_errors_wrong_plane() {
|
|
|
|
|
// RFC-010 Slice 1: --server is a data-plane addressing flag; on a
|
|
|
|
|
// storage-plane verb the guard rejects it loudly (was: silently ignored).
|
|
|
|
|
let output = output_failure(cli().arg("optimize").arg("--server").arg("prod"));
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
assert!(
|
feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)
* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)
User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.
- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
the existing exhaustive `command_plane` classifier (which stays as the drift
guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
capability-worded message. The mapping reflects *current* behavior (`queries
list` → Local, `queries validate` → Direct); it converges to the RFC end-state
table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
(storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
tests proving the allow set is exactly {Any, Served} (behavior-preservation),
the per-verb mapping, and distinct capability phrases.
Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): capability vocabulary in the CLI reference + maintenance addressing
Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 03:02:07 +03:00
|
|
|
stderr.contains("`optimize` is a direct (storage-native) command")
|
|
|
|
|
&& stderr.contains("--server/--graph address a served graph and do not apply")
|
feat(cli)!: remove legacy data-plane addressing (--target, positional http→remote, --as-on-served) (#238)
* feat(cli): --server accepts a literal URL (RFC-011 Decision 2)
`resolve_server_flag` now treats a `--server` value containing `://` as a literal
base URL (trailing slash trimmed; `--graph` appends `/graphs/<id>`), bypassing the
operator-config `servers:` registry; a bare name still resolves through the
registry. This is the replacement the upcoming `--uri http(s)://` deprecation
points at, and a small ergonomic win on its own (`--server https://host` with no
config entry). Token resolution for a literal-URL server falls to the legacy
OMNIGRAPH_BEARER_TOKEN chain, same as a positional URL today.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(cli): address the parity-matrix arms with global --store/--server flags
Prep for removing the positional-http→remote dispatch. The parity harness
addressed both arms with a positional graph right after the verb
(`omnigraph <verb> <addr> <args…>`), which only parses for top-level verbs —
for nested subcommands (`schema show`, `branch list`, …) the address landed in
the subcommand slot and BOTH arms failed identically, so the test passed
vacuously (matching exit codes, never comparing output).
Address both arms with the global flags instead — local `--store <graph>`
(embedded), remote `--server <url>` (served) — appended after the verb + args,
valid regardless of nesting. The previously-vacuous nested-verb parity checks
now actually compare embedded vs remote (and pass — parity holds), and the
remote arm no longer relies on the positional-URL dispatch that's about to be
removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli)!: --as on a served write is a hard error (was a silent no-op)
A served write resolves the actor server-side from the bearer token, so `--as`
could never set identity there — it was silently ignored. It now errors (in the
remote write factory, before any HTTP call), pointing the user at removing `--as`
or writing directly with `--store`. Reads don't carry `--as`, so this is
write-path only. BREAKING for any script that passed `--as` to a remote write
(it was a no-op, so behavior is unchanged except the now-explicit error).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli)!: a positional/--uri http(s):// URL no longer dispatches to a server
Remote graphs must be addressed with `--server <url>` (or a named server / a
profile binding one). A positional or `--uri` `http(s)://` URL on a data verb now
errors instead of silently routing to the remote HTTP client — the scheme no
longer carries transport semantics. The discriminator is `via_server`: a remote
URL produced by a server scope is fine; a remote URL from a positional/`--uri`
source is rejected (`reject_positional_remote` in both GraphClient factories).
Storage verbs are unaffected — they already reject remote URIs through
`resolve_local_graph` with the existing "direct (storage-native)" error.
Migrated the gh-host keyed-credential system test to `--server <url>` (the literal
URL still prefix-matches the operator server for token resolution). BREAKING:
scripts addressing a server by a bare URL must switch to `--server <url>`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli)!: remove the --target flag (use --store / --profile / --server)
Removes the legacy named-graph flag and threads its parameter out of the whole
resolver chain. `--target` resolved a graph name through `omnigraph.yaml`'s
`graphs:` map; its replacements (`--store <uri>`, `--profile <name>`,
`--server <name>`) all ship.
- Drops the 22 `target` clap fields + the `--cluster` exclusion that named it.
- Threads `target`/`cli_target` out of `resolve_uri`/`resolve_cli_graph`/
`resolve_local_graph`/`resolve_local_uri`/`resolve_storage_uri`/
`resolve_remote_bearer_token`/`apply_server_flag`/`execute_query_lint`/
`resolve_selected_graph`/`resolve_registry_selection_for_list`/
`execute_queries_{validate,list}`, the two `GraphClient` factories, and
`ScopeFlags`/`ResolvedScope`.
- Keeps the shared `OmnigraphConfig::resolve_target_uri` 3-arg (server boot uses
it); the CLI passes None for the explicit-target arm. The `cli.graph` default
(omnigraph.yaml bare-command fallback) is unchanged — its removal belongs to
the omnigraph.yaml excision.
- Operator/file aliases that bind a `graph` name still work: the name is now
resolved to a URI inline (a positional URI wins).
- Error messages and `--graph`/`--server`/`--store` help text no longer name
`--target`; the queries-list selection hint points at `cli.graph`.
BREAKING. Tests updated (named-target resolution rewritten onto `cli.graph`;
positional-URI tests unchanged). Full omnigraph-cli suite green (228).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): drop --target and positional-http addressing; --as-on-served is an error
Update the user docs for the legacy data-plane addressing removals:
- the CLI `--target` flag is gone — address graphs with a positional URI,
`--store`, `--profile`, or `--server <name|url>`;
- a positional `http(s)://` URI no longer dispatches to a server (use `--server`);
- `--as` on a served write is now rejected (was a silent no-op).
Touches cli/reference.md (addressing intro, capability table, error examples,
scopes), cli/index.md (the remote-read example → --server), operations/maintenance
+ policy, and the cluster docs' data-plane load guidance. The server's own
`--target` boot flag is unchanged (server.md untouched). Also fixes a pre-existing
broken maintenance link in search/indexes.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): --store is loudly exclusive with a positional URI / --server; test graphs→Served
Address two Greptile findings on the RFC-011 slices:
- Slice A (P1): `--store` combined with a positional URI silently dropped the URI
(`scope.rs` did `store.or(uri)`); `--store` + `--server` errored with a
misleading "positional URI" message. Now both combinations fail loudly with a
declared `--store is exclusive with a positional URI and --server` error.
- Slice B (P2): the `command_capability` unit test never exercised the one
Data→Served refinement (`graphs`); added the assertion so deleting that guard
can't pass silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 04:29:16 +03:00
|
|
|
&& stderr.contains("Pass a storage URI, or --cluster <dir> --cluster-graph <id>."),
|
feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)
* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)
User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.
- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
the existing exhaustive `command_plane` classifier (which stays as the drift
guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
capability-worded message. The mapping reflects *current* behavior (`queries
list` → Local, `queries validate` → Direct); it converges to the RFC end-state
table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
(storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
tests proving the allow set is exactly {Any, Served} (behavior-preservation),
the per-verb mapping, and distinct capability phrases.
Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): capability vocabulary in the CLI reference + maintenance addressing
Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 03:02:07 +03:00
|
|
|
"wrong-capability guard message not found; got: {stderr}"
|
feat(cli): RFC-010 Slice 1 — declared plane capability surface + honest addressing (#217)
* feat(cli): declared plane capability surface + wrong-plane guard (RFC-010 Slice 1)
New `planes.rs` is the single source of truth for which plane each subcommand
belongs to (Data / Storage / Control / Session). `command_plane` is an
exhaustive match — adding a `Command` variant is a compile error until its
plane is declared, so the surface cannot silently drift from the command set.
It descends into the nested enums where the plane differs per subcommand
(`schema plan` is storage while `schema show/apply` are data; `queries
validate` opens the graph while `queries list` reads only config).
`guard_addressing` runs once in `main` before dispatch: the data-plane
addressing flags `--server`/`--graph` on any non-data verb now fail with one
declared, pinned error instead of being silently ignored (`optimize --server
prod` previously dropped `--server`). `init`'s message drops the `--target`
half since it takes only a positional URI today.
Test: `cli_schema_config::schema_plan_with_server_flag_errors_wrong_plane`
pins the per-subcommand label, proving the guard descends into the nested enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): storage-plane verbs fail loudly on a remote target (RFC-010 Slice 1)
`optimize`/`repair`/`cleanup` switch from `resolve_uri` to `resolve_local_uri`,
so a `--target` (or positional URI) that resolves to a remote server now fails
with a declared storage-plane message instead of whatever `Omnigraph::open`
said about an `http(s)://` URI. The `resolve_local_graph` bail is reworded to
that storage-plane message, so every storage verb already on the local resolver
(`schema plan`, `queries validate`, `lint`) speaks with one voice.
Net: `optimize --target knowledge` resolves to the graph's storage URI and runs
embedded; `optimize --target prod` (remote) fails loudly; `optimize --server`
is caught earlier by the guard. Positional-URI invocations are unchanged.
Tests (pinned strings, per RFC-010's test plan): optimize happy path on a local
graph, `optimize --server` wrong-plane error, `optimize <https>` storage-plane
error; the existing `query_lint_rejects_http_targets_without_schema` assertion
is updated to the new shared message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:45:58 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn optimize_with_remote_target_errors_storage_plane() {
|
|
|
|
|
// RFC-010 Slice 1: a maintenance verb pointed at a remote URI fails loudly
|
|
|
|
|
// and declaratively (was: whatever Omnigraph::open said about an https URI).
|
|
|
|
|
let output = output_failure(cli().arg("optimize").arg("https://graph.example.invalid"));
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
assert!(
|
feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)
* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)
User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.
- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
the existing exhaustive `command_plane` classifier (which stays as the drift
guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
capability-worded message. The mapping reflects *current* behavior (`queries
list` → Local, `queries validate` → Direct); it converges to the RFC end-state
table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
(storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
tests proving the allow set is exactly {Any, Served} (behavior-preservation),
the per-verb mapping, and distinct capability phrases.
Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): capability vocabulary in the CLI reference + maintenance addressing
Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 03:02:07 +03:00
|
|
|
stderr.contains("`optimize` is a direct (storage-native) command and needs direct storage access")
|
feat(cli): RFC-010 Slice 1 — declared plane capability surface + honest addressing (#217)
* feat(cli): declared plane capability surface + wrong-plane guard (RFC-010 Slice 1)
New `planes.rs` is the single source of truth for which plane each subcommand
belongs to (Data / Storage / Control / Session). `command_plane` is an
exhaustive match — adding a `Command` variant is a compile error until its
plane is declared, so the surface cannot silently drift from the command set.
It descends into the nested enums where the plane differs per subcommand
(`schema plan` is storage while `schema show/apply` are data; `queries
validate` opens the graph while `queries list` reads only config).
`guard_addressing` runs once in `main` before dispatch: the data-plane
addressing flags `--server`/`--graph` on any non-data verb now fail with one
declared, pinned error instead of being silently ignored (`optimize --server
prod` previously dropped `--server`). `init`'s message drops the `--target`
half since it takes only a positional URI today.
Test: `cli_schema_config::schema_plan_with_server_flag_errors_wrong_plane`
pins the per-subcommand label, proving the guard descends into the nested enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): storage-plane verbs fail loudly on a remote target (RFC-010 Slice 1)
`optimize`/`repair`/`cleanup` switch from `resolve_uri` to `resolve_local_uri`,
so a `--target` (or positional URI) that resolves to a remote server now fails
with a declared storage-plane message instead of whatever `Omnigraph::open`
said about an `http(s)://` URI. The `resolve_local_graph` bail is reworded to
that storage-plane message, so every storage verb already on the local resolver
(`schema plan`, `queries validate`, `lint`) speaks with one voice.
Net: `optimize --target knowledge` resolves to the graph's storage URI and runs
embedded; `optimize --target prod` (remote) fails loudly; `optimize --server`
is caught earlier by the guard. Positional-URI invocations are unchanged.
Tests (pinned strings, per RFC-010's test plan): optimize happy path on a local
graph, `optimize --server` wrong-plane error, `optimize <https>` storage-plane
error; the existing `query_lint_rejects_http_targets_without_schema` assertion
is updated to the new shared message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:45:58 +03:00
|
|
|
&& stderr.contains("remote server"),
|
feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)
* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)
User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.
- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
the existing exhaustive `command_plane` classifier (which stays as the drift
guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
capability-worded message. The mapping reflects *current* behavior (`queries
list` → Local, `queries validate` → Direct); it converges to the RFC end-state
table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
(storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
tests proving the allow set is exactly {Any, Served} (behavior-preservation),
the per-verb mapping, and distinct capability phrases.
Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): capability vocabulary in the CLI reference + maintenance addressing
Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 03:02:07 +03:00
|
|
|
"direct remote-target message not found; got: {stderr}"
|
feat(cli): RFC-010 Slice 1 — declared plane capability surface + honest addressing (#217)
* feat(cli): declared plane capability surface + wrong-plane guard (RFC-010 Slice 1)
New `planes.rs` is the single source of truth for which plane each subcommand
belongs to (Data / Storage / Control / Session). `command_plane` is an
exhaustive match — adding a `Command` variant is a compile error until its
plane is declared, so the surface cannot silently drift from the command set.
It descends into the nested enums where the plane differs per subcommand
(`schema plan` is storage while `schema show/apply` are data; `queries
validate` opens the graph while `queries list` reads only config).
`guard_addressing` runs once in `main` before dispatch: the data-plane
addressing flags `--server`/`--graph` on any non-data verb now fail with one
declared, pinned error instead of being silently ignored (`optimize --server
prod` previously dropped `--server`). `init`'s message drops the `--target`
half since it takes only a positional URI today.
Test: `cli_schema_config::schema_plan_with_server_flag_errors_wrong_plane`
pins the per-subcommand label, proving the guard descends into the nested enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): storage-plane verbs fail loudly on a remote target (RFC-010 Slice 1)
`optimize`/`repair`/`cleanup` switch from `resolve_uri` to `resolve_local_uri`,
so a `--target` (or positional URI) that resolves to a remote server now fails
with a declared storage-plane message instead of whatever `Omnigraph::open`
said about an `http(s)://` URI. The `resolve_local_graph` bail is reworded to
that storage-plane message, so every storage verb already on the local resolver
(`schema plan`, `queries validate`, `lint`) speaks with one voice.
Net: `optimize --target knowledge` resolves to the graph's storage URI and runs
embedded; `optimize --target prod` (remote) fails loudly; `optimize --server`
is caught earlier by the guard. Positional-URI invocations are unchanged.
Tests (pinned strings, per RFC-010's test plan): optimize happy path on a local
graph, `optimize --server` wrong-plane error, `optimize <https>` storage-plane
error; the existing `query_lint_rejects_http_targets_without_schema` assertion
is updated to the new shared message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:45:58 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:16:51 +03:00
|
|
|
#[test]
|
|
|
|
|
fn repair_json_reports_noop_on_clean_graph() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_success(cli().arg("repair").arg("--json").arg(&graph));
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["confirm"], false);
|
|
|
|
|
assert_eq!(payload["force"], false);
|
|
|
|
|
assert_eq!(payload["manifest_version"], Value::Null);
|
|
|
|
|
let tables = payload["tables"].as_array().unwrap();
|
|
|
|
|
assert_eq!(tables.len(), 4);
|
|
|
|
|
assert!(tables.iter().all(|table| {
|
|
|
|
|
table["classification"] == "no_drift" && table["action"] == "no_op"
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn repair_confirm_json_refuses_suspicious_drift_with_nonzero_exit_then_force_succeeds() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
let graph_manifest_before = manifest_dataset_version(&graph);
|
|
|
|
|
let (table_manifest_before, table_head_before) = forge_person_delete_drift(&graph);
|
|
|
|
|
|
|
|
|
|
let refused = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("repair")
|
|
|
|
|
.arg("--confirm")
|
|
|
|
|
.arg("--json")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
let refused_payload: Value = serde_json::from_slice(&refused.stdout).unwrap();
|
|
|
|
|
assert_eq!(refused_payload["manifest_version"], Value::Null);
|
|
|
|
|
let person = refused_payload["tables"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|table| table["table_key"] == "node:Person")
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(person["classification"], "suspicious");
|
|
|
|
|
assert_eq!(person["action"], "refused");
|
|
|
|
|
assert!(
|
|
|
|
|
String::from_utf8_lossy(&refused.stderr).contains("repair refused"),
|
|
|
|
|
"stderr should explain the non-zero exit; got: {}",
|
|
|
|
|
String::from_utf8_lossy(&refused.stderr)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(manifest_dataset_version(&graph), graph_manifest_before);
|
|
|
|
|
|
|
|
|
|
let forced = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("repair")
|
|
|
|
|
.arg("--force")
|
|
|
|
|
.arg("--confirm")
|
|
|
|
|
.arg("--json")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
let forced_payload: Value = serde_json::from_slice(&forced.stdout).unwrap();
|
|
|
|
|
let forced_manifest = forced_payload["manifest_version"].as_u64().unwrap();
|
|
|
|
|
assert!(forced_manifest > graph_manifest_before);
|
|
|
|
|
let person = forced_payload["tables"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|table| table["table_key"] == "node:Person")
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(person["classification"], "suspicious");
|
|
|
|
|
assert_eq!(person["action"], "forced");
|
|
|
|
|
assert_eq!(person["manifest_version"], table_manifest_before);
|
|
|
|
|
assert_eq!(person["lance_head_version"], table_head_before);
|
|
|
|
|
assert_eq!(manifest_dataset_version(&graph), forced_manifest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn query_lint_json_with_schema_reports_warnings() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let schema_path = temp.path().join("schema.pg");
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
write_file(
|
|
|
|
|
&schema_path,
|
|
|
|
|
r#"
|
|
|
|
|
node Policy {
|
|
|
|
|
slug: String @key
|
|
|
|
|
name: String?
|
|
|
|
|
effectiveTo: DateTime?
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query update_policy($slug: String, $name: String) {
|
|
|
|
|
update Policy set { name: $name } where slug = $slug
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["status"], "ok");
|
|
|
|
|
assert_eq!(payload["schema_source"]["kind"], "file");
|
|
|
|
|
assert_eq!(payload["queries_processed"], 1);
|
|
|
|
|
assert_eq!(payload["warnings"], 1);
|
|
|
|
|
assert_eq!(payload["findings"][0]["code"], "L201");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
payload["findings"][0]["message"],
|
|
|
|
|
"Policy.effectiveTo exists in schema but no update query sets it"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn lint_top_level_matches_deprecated_query_lint_output() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let schema_path = temp.path().join("schema.pg");
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
write_file(
|
|
|
|
|
&schema_path,
|
|
|
|
|
r#"
|
|
|
|
|
node Person {
|
|
|
|
|
name: String
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query list_people() {
|
|
|
|
|
match { $p: Person }
|
|
|
|
|
return { $p.name }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let canonical = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let deprecated_lint = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let deprecated_check = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("check")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(stdout_string(&canonical), stdout_string(&deprecated_lint));
|
|
|
|
|
assert_eq!(stdout_string(&canonical), stdout_string(&deprecated_check));
|
|
|
|
|
|
|
|
|
|
// Canonical form must NOT emit the deprecation warning.
|
|
|
|
|
let canonical_stderr = String::from_utf8(canonical.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
!canonical_stderr.contains("deprecated"),
|
|
|
|
|
"`omnigraph lint` is canonical and must not warn; got stderr: {canonical_stderr}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Deprecated forms MUST emit the one-line warning, pointing at the
|
|
|
|
|
// new top-level `omnigraph lint`.
|
|
|
|
|
let lint_stderr = String::from_utf8(deprecated_lint.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
lint_stderr.contains("`omnigraph query lint` is deprecated")
|
|
|
|
|
&& lint_stderr.contains("`omnigraph lint`"),
|
|
|
|
|
"expected deprecation warning pointing at `omnigraph lint`; got: {lint_stderr}"
|
|
|
|
|
);
|
|
|
|
|
let check_stderr = String::from_utf8(deprecated_check.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
check_stderr.contains("`omnigraph query check` is deprecated")
|
|
|
|
|
&& check_stderr.contains("`omnigraph lint`"),
|
|
|
|
|
"expected deprecation warning pointing at `omnigraph lint`; got: {check_stderr}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deprecated_check_top_level_rewrites_to_lint() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let schema_path = temp.path().join("schema.pg");
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
write_file(
|
|
|
|
|
&schema_path,
|
|
|
|
|
r#"
|
|
|
|
|
node Person {
|
|
|
|
|
name: String
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query list_people() {
|
|
|
|
|
match { $p: Person }
|
|
|
|
|
return { $p.name }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let canonical = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let deprecated_check = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("check")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(stdout_string(&canonical), stdout_string(&deprecated_check));
|
|
|
|
|
|
|
|
|
|
let check_stderr = String::from_utf8(deprecated_check.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
check_stderr.contains("`omnigraph check` is deprecated")
|
|
|
|
|
&& check_stderr.contains("`omnigraph lint`"),
|
|
|
|
|
"expected `omnigraph check` deprecation warning pointing at `omnigraph lint`; got: {check_stderr}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// `check` must NOT appear in the canonical `omnigraph --help` output —
|
|
|
|
|
// agents copy the surface from help text and would otherwise emit both
|
|
|
|
|
// names interchangeably.
|
|
|
|
|
let help = cli().arg("--help").output().unwrap();
|
|
|
|
|
let stdout = String::from_utf8(help.stdout).unwrap();
|
|
|
|
|
let check_aliased = stdout
|
|
|
|
|
.lines()
|
|
|
|
|
.any(|line| line.trim_start().starts_with("lint") && line.contains("check"));
|
|
|
|
|
assert!(
|
|
|
|
|
!check_aliased,
|
|
|
|
|
"`check` must not be advertised as a visible alias of `lint`; help output: {stdout}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deprecated_read_and_change_subcommands_emit_warnings() {
|
|
|
|
|
// Both subcommands require `--query`/`--query-string`/`--alias`, so
|
|
|
|
|
// invoking them with no args will exit non-zero. That's fine --
|
|
|
|
|
// we only care that the deprecation warning is printed before the
|
|
|
|
|
// argument-required error.
|
|
|
|
|
let output = cli().arg("read").output().unwrap();
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
stderr.contains("`omnigraph read` is deprecated") && stderr.contains("`omnigraph query`"),
|
|
|
|
|
"expected `omnigraph read` deprecation warning; got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = cli().arg("change").output().unwrap();
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
stderr.contains("`omnigraph change` is deprecated")
|
|
|
|
|
&& stderr.contains("`omnigraph mutate`"),
|
|
|
|
|
"expected `omnigraph change` deprecation warning; got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Sanity check the inverse: the canonical names must NOT print the
|
|
|
|
|
// deprecation banner.
|
|
|
|
|
let output = cli().arg("query").arg("--help").output().unwrap();
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
!stderr.contains("deprecated"),
|
|
|
|
|
"`omnigraph query` is canonical and must not warn; got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
let output = cli().arg("mutate").arg("--help").output().unwrap();
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
!stderr.contains("deprecated"),
|
|
|
|
|
"`omnigraph mutate` is canonical and must not warn; got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn query_lint_can_use_local_graph_via_positional_uri() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query list_people() {
|
|
|
|
|
match { $p: Person }
|
|
|
|
|
return { $p.name }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--json")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["status"], "ok");
|
|
|
|
|
assert_eq!(payload["schema_source"]["kind"], "graph");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
payload["schema_source"]["uri"].as_str(),
|
|
|
|
|
Some(graph.to_string_lossy().as_ref())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn query_lint_can_resolve_graph_and_query_from_config() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
let config_path = temp.path().join("omnigraph.yaml");
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
write_query_file(
|
|
|
|
|
&temp.path().join("queries.gq"),
|
|
|
|
|
r#"
|
|
|
|
|
query list_people() {
|
|
|
|
|
match { $p: Person }
|
|
|
|
|
return { $p.name }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
write_config(&config_path, &local_yaml_config(&graph));
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg("queries.gq")
|
|
|
|
|
.arg("--config")
|
|
|
|
|
.arg(&config_path)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["status"], "ok");
|
|
|
|
|
assert_eq!(payload["schema_source"]["kind"], "graph");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
payload["schema_source"]["uri"].as_str(),
|
|
|
|
|
Some(graph.to_string_lossy().as_ref())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn query_lint_rejects_http_targets_without_schema() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query list_people() {
|
|
|
|
|
match { $p: Person }
|
|
|
|
|
return { $p.name }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("http://127.0.0.1:8080"),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)
* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)
User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.
- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
the existing exhaustive `command_plane` classifier (which stays as the drift
guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
capability-worded message. The mapping reflects *current* behavior (`queries
list` → Local, `queries validate` → Direct); it converges to the RFC end-state
table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
(storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
tests proving the allow set is exactly {Any, Served} (behavior-preservation),
the per-verb mapping, and distinct capability phrases.
Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): capability vocabulary in the CLI reference + maintenance addressing
Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 03:02:07 +03:00
|
|
|
// RFC-010/011: the direct (storage-native) verbs share one declared message
|
feat(cli): RFC-010 Slice 1 — declared plane capability surface + honest addressing (#217)
* feat(cli): declared plane capability surface + wrong-plane guard (RFC-010 Slice 1)
New `planes.rs` is the single source of truth for which plane each subcommand
belongs to (Data / Storage / Control / Session). `command_plane` is an
exhaustive match — adding a `Command` variant is a compile error until its
plane is declared, so the surface cannot silently drift from the command set.
It descends into the nested enums where the plane differs per subcommand
(`schema plan` is storage while `schema show/apply` are data; `queries
validate` opens the graph while `queries list` reads only config).
`guard_addressing` runs once in `main` before dispatch: the data-plane
addressing flags `--server`/`--graph` on any non-data verb now fail with one
declared, pinned error instead of being silently ignored (`optimize --server
prod` previously dropped `--server`). `init`'s message drops the `--target`
half since it takes only a positional URI today.
Test: `cli_schema_config::schema_plan_with_server_flag_errors_wrong_plane`
pins the per-subcommand label, proving the guard descends into the nested enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): storage-plane verbs fail loudly on a remote target (RFC-010 Slice 1)
`optimize`/`repair`/`cleanup` switch from `resolve_uri` to `resolve_local_uri`,
so a `--target` (or positional URI) that resolves to a remote server now fails
with a declared storage-plane message instead of whatever `Omnigraph::open`
said about an `http(s)://` URI. The `resolve_local_graph` bail is reworded to
that storage-plane message, so every storage verb already on the local resolver
(`schema plan`, `queries validate`, `lint`) speaks with one voice.
Net: `optimize --target knowledge` resolves to the graph's storage URI and runs
embedded; `optimize --target prod` (remote) fails loudly; `optimize --server`
is caught earlier by the guard. Positional-URI invocations are unchanged.
Tests (pinned strings, per RFC-010's test plan): optimize happy path on a local
graph, `optimize --server` wrong-plane error, `optimize <https>` storage-plane
error; the existing `query_lint_rejects_http_targets_without_schema` assertion
is updated to the new shared message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:45:58 +03:00
|
|
|
// (was: "query lint is only supported against local graph URIs …").
|
2026-06-11 15:16:51 +03:00
|
|
|
assert!(
|
feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)
* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)
User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.
- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
the existing exhaustive `command_plane` classifier (which stays as the drift
guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
capability-worded message. The mapping reflects *current* behavior (`queries
list` → Local, `queries validate` → Direct); it converges to the RFC end-state
table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
(storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
tests proving the allow set is exactly {Any, Served} (behavior-preservation),
the per-verb mapping, and distinct capability phrases.
Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): capability vocabulary in the CLI reference + maintenance addressing
Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 03:02:07 +03:00
|
|
|
stderr.contains("`lint` is a direct (storage-native) command and needs direct storage access")
|
feat(cli): RFC-010 Slice 1 — declared plane capability surface + honest addressing (#217)
* feat(cli): declared plane capability surface + wrong-plane guard (RFC-010 Slice 1)
New `planes.rs` is the single source of truth for which plane each subcommand
belongs to (Data / Storage / Control / Session). `command_plane` is an
exhaustive match — adding a `Command` variant is a compile error until its
plane is declared, so the surface cannot silently drift from the command set.
It descends into the nested enums where the plane differs per subcommand
(`schema plan` is storage while `schema show/apply` are data; `queries
validate` opens the graph while `queries list` reads only config).
`guard_addressing` runs once in `main` before dispatch: the data-plane
addressing flags `--server`/`--graph` on any non-data verb now fail with one
declared, pinned error instead of being silently ignored (`optimize --server
prod` previously dropped `--server`). `init`'s message drops the `--target`
half since it takes only a positional URI today.
Test: `cli_schema_config::schema_plan_with_server_flag_errors_wrong_plane`
pins the per-subcommand label, proving the guard descends into the nested enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): storage-plane verbs fail loudly on a remote target (RFC-010 Slice 1)
`optimize`/`repair`/`cleanup` switch from `resolve_uri` to `resolve_local_uri`,
so a `--target` (or positional URI) that resolves to a remote server now fails
with a declared storage-plane message instead of whatever `Omnigraph::open`
said about an `http(s)://` URI. The `resolve_local_graph` bail is reworded to
that storage-plane message, so every storage verb already on the local resolver
(`schema plan`, `queries validate`, `lint`) speaks with one voice.
Net: `optimize --target knowledge` resolves to the graph's storage URI and runs
embedded; `optimize --target prod` (remote) fails loudly; `optimize --server`
is caught earlier by the guard. Positional-URI invocations are unchanged.
Tests (pinned strings, per RFC-010's test plan): optimize happy path on a local
graph, `optimize --server` wrong-plane error, `optimize <https>` storage-plane
error; the existing `query_lint_rejects_http_targets_without_schema` assertion
is updated to the new shared message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:45:58 +03:00
|
|
|
&& stderr.contains("remote server"),
|
feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local) (#237)
* feat(cli): RFC-011 Slice B — capability vocabulary (any/served/direct/control/local)
User-facing CLI errors and --help now speak a single "capability" vocabulary —
what a command needs — instead of the internal four-plane jargon. Behavior is
unchanged: the --server/--graph allow set is identical (the served-graph
capabilities `any` ∪ `served` = the old `Data` plane, since `graphs` was already
allowed). Only error text and the --help legend change.
- planes.rs: add `Capability { Any, Served, Direct, Control, Local }` derived from
the existing exhaustive `command_plane` classifier (which stays as the drift
guard) plus the one Data→Served refinement (`graphs`). `guard_addressing` now
allows `--server`/`--graph` on `{Any, Served}` and rejects elsewhere with a
capability-worded message. The mapping reflects *current* behavior (`queries
list` → Local, `queries validate` → Direct); it converges to the RFC end-state
table when later slices re-route those verbs.
- scope.rs: `resolve_scope` takes `Capability` instead of `Plane`, so the whole
addressing path speaks one vocabulary; call sites in client.rs (Any) and the 3
maintenance verbs in main.rs (Direct) updated.
- helpers.rs: the storage-direct remote rejection reworded to "direct
(storage-native) command".
- cli.rs: the --help legend is now "COMMANDS BY CAPABILITY".
- Tests: the 5 assertions pinning the old plane text updated; added planes.rs unit
tests proving the allow set is exactly {Any, Served} (behavior-preservation),
the per-verb mapping, and distinct capability phrases.
Full omnigraph-cli suite: 225 green (222 + 3 new), zero behavior-test changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): capability vocabulary in the CLI reference + maintenance addressing
Rename the reference's "Command planes" section to "Command capabilities"
(any/served/direct/control/local), reword the error examples, and update the
maintenance doc's addressing note + its section cross-link to match Slice B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 03:02:07 +03:00
|
|
|
"direct remote-target message not found; got: {stderr}"
|
2026-06-11 15:16:51 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn query_lint_requires_schema_or_resolvable_graph_target() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query list_people() {
|
|
|
|
|
match { $p: Person }
|
|
|
|
|
return { $p.name }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
assert!(
|
2026-06-13 22:58:51 +03:00
|
|
|
stderr.contains("lint requires --schema <schema.pg> or a resolvable graph target")
|
2026-06-11 15:16:51 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn query_lint_human_output_reports_warnings() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let schema_path = temp.path().join("schema.pg");
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
write_file(
|
|
|
|
|
&schema_path,
|
|
|
|
|
r#"
|
|
|
|
|
node Policy {
|
|
|
|
|
slug: String @key
|
|
|
|
|
name: String?
|
|
|
|
|
effectiveTo: DateTime?
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query update_policy($slug: String, $name: String) {
|
|
|
|
|
update Policy set { name: $name } where slug = $slug
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path),
|
|
|
|
|
);
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert!(stdout.contains("OK query `update_policy` (mutation)"));
|
|
|
|
|
assert!(
|
|
|
|
|
stdout.contains("WARN Policy.effectiveTo exists in schema but no update query sets it")
|
|
|
|
|
);
|
|
|
|
|
assert!(stdout.contains(
|
|
|
|
|
"INFO Lint complete: 1 queries processed (0 error(s), 1 warning(s), 0 info item(s))"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn query_lint_human_output_reports_strict_validation_errors() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let schema_path = temp.path().join("schema.pg");
|
|
|
|
|
let query_path = temp.path().join("queries.gq");
|
|
|
|
|
write_file(
|
|
|
|
|
&schema_path,
|
|
|
|
|
r#"
|
|
|
|
|
node Policy {
|
|
|
|
|
slug: String @key
|
|
|
|
|
name: String?
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
write_query_file(
|
|
|
|
|
&query_path,
|
|
|
|
|
r#"
|
|
|
|
|
query bad_update($slug: String) {
|
|
|
|
|
update Policy set { priority_level: "high" } where slug = $slug
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("lint")
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&query_path)
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&schema_path),
|
|
|
|
|
);
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert!(stdout.contains("ERROR query `bad_update`:"));
|
|
|
|
|
assert!(stdout.contains("Policy"));
|
|
|
|
|
assert!(stdout.contains(
|
|
|
|
|
"INFO Lint complete: 1 queries processed (1 error(s), 0 warning(s), 0 info item(s))"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn load_json_outputs_summary_for_main_branch() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
let data = fixture("test.jsonl");
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("load")
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
.arg("overwrite")
|
|
|
|
|
.arg("--data")
|
|
|
|
|
.arg(&data)
|
|
|
|
|
.arg("--json")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["branch"], "main");
|
|
|
|
|
assert_eq!(payload["mode"], "overwrite");
|
|
|
|
|
assert_eq!(payload["nodes_loaded"], 6);
|
|
|
|
|
assert_eq!(payload["edges_loaded"], 5);
|
|
|
|
|
assert_eq!(payload["node_types_loaded"], 2);
|
|
|
|
|
assert_eq!(payload["edge_types_loaded"], 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn load_into_feature_branch_with_merge_mode_succeeds() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let feature_data = temp.path().join("feature.jsonl");
|
|
|
|
|
write_jsonl(
|
|
|
|
|
&feature_data,
|
|
|
|
|
r#"{"type":"Person","data":{"name":"Alice","age":31}}"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("load")
|
|
|
|
|
.arg("--data")
|
|
|
|
|
.arg(&feature_data)
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
.arg("merge")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert!(stdout.contains("branch feature"));
|
|
|
|
|
assert!(stdout.contains("with merge"));
|
|
|
|
|
assert!(stdout.contains("1 nodes across 1 node types"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_json_outputs_rows_for_named_query() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
let queries = fixture("test.gq");
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&queries)
|
|
|
|
|
.arg("--name")
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["query_name"], "get_person");
|
|
|
|
|
assert_eq!(payload["target"]["branch"], "main");
|
|
|
|
|
assert_eq!(payload["row_count"], 1);
|
|
|
|
|
assert_eq!(payload["rows"][0]["p.name"], "Alice");
|
|
|
|
|
}
|
|
|
|
|
|
feat(cli): RFC-011 Slice A — additive scope/profile addressing (#235)
* feat(cli): RFC-011 Slice A — operator-config scope structs (profiles/clusters/defaults)
Additive operator-config surface for the RFC-011 scope model. No behavior
change yet — these structs are parsed but not consumed until the scope
resolver lands.
- OperatorConfig gains `profiles:` (name → OperatorProfile) and `clusters:`
(name → OperatorCluster { root }) — the latter the only place a storage
root appears in operator config (RFC-011 storage-root rule).
- OperatorDefaults gains `server` and `default_graph` (the flat-default scope).
- OperatorProfile binds one of {server, cluster, store} + default_graph;
`binding()` validates exactly-one on use and returns a ScopeBinding.
- Accessors profile()/cluster_root()/default_server()/default_graph();
unknown-key warnings extended to the new blocks (forward-compat preserved —
old configs still load, new keys are no longer "unknown").
Tests: parse profiles/clusters/scope-defaults, binding rejects zero/multiple
entities, unknown keys in a profile warn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): RFC-011 Slice A — scope resolver + --profile/--store, wired (additive)
Translate the new scope inputs into the existing addressing tuple, in front of
the unchanged resolvers. Purely additive: an explicit address
(--uri/--target/--server/--store) passes straight through, so every existing
invocation is byte-for-byte unchanged.
- scope.rs: resolve_scope() with the RFC-011 precedence (explicit > --profile /
OMNIGRAPH_PROFILE > flat defaults.server), producing the effective
(server, graph, uri, target) for data verbs and (cluster, cluster_graph) for
maintenance. Plane×scope capability check (server scope rejected on a
maintenance verb; cluster scope rejected on a data verb; store rejects --graph)
fires only on the new paths. 9 unit tests.
- cli.rs: global --profile <NAME> and --store <URI>. (--graph keeps
requires=server for now; profile/default graph comes from default_graph —
profile+--graph override is deferred to the --cluster-graph rework.)
- client.rs: the two GraphClient factories call resolve_scope (Plane::Data) up
front; the explicit branch reproduces today's behavior exactly.
- main.rs: the 15 data call sites forward --profile/--store; the 3 maintenance
verbs consult the scope (Plane::Storage) only when no explicit per-command
address is given, so cluster-binding profiles and --store reach
optimize/repair/cleanup.
Verified: the full omnigraph-cli suite (221 tests) stays green untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test+docs(cli): RFC-011 Slice A — end-to-end scope test + reference docs
- cli_data.rs: prove --store and a --profile store binding drive a read
identically to the legacy positional URI (the additive-coexistence contract),
end to end against a local graph (no server needed).
- cli/reference.md: document profiles/clusters/defaults.server/default_graph,
the --profile/--store flags, and a "Scopes & profiles" section; note the model
coexists with legacy addressing (nothing removed yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:37:55 +03:00
|
|
|
#[test]
|
|
|
|
|
fn read_via_store_flag_and_profile_match_positional_uri() {
|
|
|
|
|
// RFC-011 Slice A: the new scope addressing (--store, and a --profile that
|
|
|
|
|
// binds a store) drives a read identically to the legacy positional URI —
|
|
|
|
|
// the scope layer is additive, not a behavior change.
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
let queries = fixture("test.gq");
|
|
|
|
|
|
|
|
|
|
let read_rows = |cmd: &mut Command| -> Value {
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cmd.arg("--query")
|
|
|
|
|
.arg(&queries)
|
|
|
|
|
.arg("--name")
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
serde_json::from_slice(&output.stdout).unwrap()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Baseline: positional URI.
|
|
|
|
|
let baseline = read_rows(cli().arg("query").arg(&graph));
|
|
|
|
|
assert_eq!(baseline["rows"][0]["p.name"], "Alice");
|
|
|
|
|
|
|
|
|
|
// --store names the same graph directly.
|
|
|
|
|
let via_store = read_rows(cli().arg("query").arg("--store").arg(&graph));
|
|
|
|
|
assert_eq!(via_store["rows"], baseline["rows"]);
|
|
|
|
|
|
|
|
|
|
// A profile binding that store, selected with --profile (no positional).
|
|
|
|
|
let home = temp.path().join("op-home");
|
|
|
|
|
std::fs::create_dir_all(&home).unwrap();
|
|
|
|
|
std::fs::write(
|
|
|
|
|
home.join("config.yaml"),
|
|
|
|
|
format!(
|
|
|
|
|
"profiles:\n local:\n store: '{}'\n",
|
|
|
|
|
graph.to_string_lossy()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let via_profile = read_rows(
|
|
|
|
|
cli()
|
|
|
|
|
.env("OMNIGRAPH_HOME", &home)
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("--profile")
|
|
|
|
|
.arg("local"),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(via_profile["rows"], baseline["rows"]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:16:51 +03:00
|
|
|
#[test]
|
|
|
|
|
fn export_jsonl_outputs_source_rows_for_selected_branch_and_type() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let feature_data = temp.path().join("feature-export.jsonl");
|
|
|
|
|
write_jsonl(
|
|
|
|
|
&feature_data,
|
|
|
|
|
r#"{"type":"Person","data":{"name":"Eve","age":29}}"#,
|
|
|
|
|
);
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("load")
|
|
|
|
|
.arg("--data")
|
|
|
|
|
.arg(&feature_data)
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
.arg("append")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("export")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--type")
|
|
|
|
|
.arg("Person")
|
|
|
|
|
.arg("--jsonl"),
|
|
|
|
|
);
|
|
|
|
|
let rows = stdout_string(&output)
|
|
|
|
|
.lines()
|
|
|
|
|
.map(|line| serde_json::from_str::<Value>(line).unwrap())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
assert_eq!(rows.len(), 5);
|
|
|
|
|
assert!(rows.iter().all(|row| row["type"] == "Person"));
|
|
|
|
|
assert!(rows.iter().all(|row| row.get("edge").is_none()));
|
|
|
|
|
assert!(
|
|
|
|
|
rows.iter()
|
|
|
|
|
.any(|row| row["data"]["name"].as_str() == Some("Eve"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn policy_validate_accepts_valid_policy_file() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let (config, _) = write_policy_config_fixture(temp.path());
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("policy")
|
|
|
|
|
.arg("validate")
|
|
|
|
|
.arg("--config")
|
|
|
|
|
.arg(&config),
|
|
|
|
|
);
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert!(stdout.contains("policy valid:"));
|
|
|
|
|
assert!(stdout.contains("policy.yaml"));
|
|
|
|
|
assert!(stdout.contains("[2 actors]"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn policy_validate_fails_for_invalid_policy_file() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
|
|
|
let policy = temp.path().join("policy.yaml");
|
|
|
|
|
fs::write(
|
|
|
|
|
&config,
|
|
|
|
|
r#"
|
|
|
|
|
project:
|
|
|
|
|
name: policy-test-graph
|
|
|
|
|
policy:
|
|
|
|
|
file: ./policy.yaml
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
&policy,
|
|
|
|
|
r#"
|
|
|
|
|
version: 1
|
|
|
|
|
groups:
|
|
|
|
|
team: [act-andrew]
|
|
|
|
|
rules:
|
|
|
|
|
- id: duplicate
|
|
|
|
|
allow:
|
|
|
|
|
actors: { group: team }
|
|
|
|
|
actions: [read]
|
|
|
|
|
branch_scope: any
|
|
|
|
|
- id: duplicate
|
|
|
|
|
allow:
|
|
|
|
|
actors: { group: team }
|
|
|
|
|
actions: [export]
|
|
|
|
|
branch_scope: any
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("policy")
|
|
|
|
|
.arg("validate")
|
|
|
|
|
.arg("--config")
|
|
|
|
|
.arg(&config),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(stderr.contains("duplicate policy rule id"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn policy_test_runs_declarative_cases() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let (config, _) = write_policy_config_fixture(temp.path());
|
|
|
|
|
|
|
|
|
|
let output = output_success(cli().arg("policy").arg("test").arg("--config").arg(&config));
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert!(stdout.contains("policy tests passed: 2 cases"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn policy_explain_reports_decision_and_matched_rule() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let (config, _) = write_policy_config_fixture(temp.path());
|
|
|
|
|
|
|
|
|
|
let allow = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("policy")
|
|
|
|
|
.arg("explain")
|
|
|
|
|
.arg("--config")
|
|
|
|
|
.arg(&config)
|
|
|
|
|
.arg("--actor")
|
|
|
|
|
.arg("act-andrew")
|
|
|
|
|
.arg("--action")
|
|
|
|
|
.arg("change")
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
);
|
|
|
|
|
let allow_stdout = stdout_string(&allow);
|
|
|
|
|
assert!(allow_stdout.contains("decision: allow"));
|
|
|
|
|
assert!(allow_stdout.contains("matched_rule: team-write"));
|
|
|
|
|
|
|
|
|
|
let deny = 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 deny_stdout = stdout_string(&deny);
|
|
|
|
|
assert!(deny_stdout.contains("decision: deny"));
|
|
|
|
|
assert!(deny_stdout.contains("message: policy denied action 'change' on branch 'main'"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_can_resolve_uri_from_config() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
write_config(&config, &local_yaml_config(&graph));
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg("--config")
|
|
|
|
|
.arg(&config)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
.arg("--name")
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["row_count"], 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_csv_format_outputs_header_and_row_values() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
.arg("--name")
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
.arg("--format")
|
|
|
|
|
.arg("csv"),
|
|
|
|
|
);
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert!(stdout.lines().next().unwrap().contains("p.name"));
|
|
|
|
|
assert!(stdout.contains("Alice"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 20:29:02 +03:00
|
|
|
/// RFC-007 PR 1: the format cascade's operator hop — `defaults.output` in
|
|
|
|
|
/// ~/.omnigraph/config.yaml applies when nothing more specific is given,
|
|
|
|
|
/// and `--format` still wins over it.
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_uses_operator_default_output_format() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
let operator_home = tempdir().unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
operator_home.path().join("config.yaml"),
|
|
|
|
|
"defaults:\n output: csv\n",
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let read = |extra: &[&str]| {
|
|
|
|
|
let mut command = cli();
|
|
|
|
|
command
|
|
|
|
|
.env("OMNIGRAPH_HOME", operator_home.path())
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
.arg("--name")
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#);
|
|
|
|
|
for arg in extra {
|
|
|
|
|
command.arg(arg);
|
|
|
|
|
}
|
|
|
|
|
stdout_string(&output_success(&mut command))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let stdout = read(&[]);
|
|
|
|
|
assert!(
|
|
|
|
|
stdout.lines().next().unwrap().contains("p.name") && stdout.contains("Alice"),
|
|
|
|
|
"operator defaults.output: csv applies with no --format: {stdout}"
|
|
|
|
|
);
|
|
|
|
|
let stdout = read(&["--format", "jsonl"]);
|
|
|
|
|
assert!(
|
|
|
|
|
stdout.starts_with('{'),
|
|
|
|
|
"--format wins over the operator default: {stdout}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:16:51 +03:00
|
|
|
#[test]
|
|
|
|
|
fn read_jsonl_format_outputs_metadata_header_first() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
.arg("--name")
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
.arg("--format")
|
|
|
|
|
.arg("jsonl"),
|
|
|
|
|
);
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
let mut lines = stdout.lines();
|
|
|
|
|
assert!(lines.next().unwrap().contains("\"kind\":\"metadata\""));
|
|
|
|
|
assert!(lines.next().unwrap().contains("\"p.name\":\"Alice\""));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn change_json_outputs_affected_counts_and_persists() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
let mutation_file = temp.path().join("mutations.gq");
|
|
|
|
|
write_query_file(
|
|
|
|
|
&mutation_file,
|
|
|
|
|
r#"
|
|
|
|
|
query insert_person($name: String, $age: I32) {
|
|
|
|
|
insert Person { name: $name, age: $age }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("change")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Eve","age":29}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["branch"], "main");
|
|
|
|
|
assert_eq!(payload["query_name"], "insert_person");
|
|
|
|
|
assert_eq!(payload["affected_nodes"], 1);
|
|
|
|
|
assert_eq!(payload["affected_edges"], 0);
|
|
|
|
|
|
|
|
|
|
let verify = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
.arg("--name")
|
|
|
|
|
.arg("get_person")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Eve"}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let verify_payload: Value = serde_json::from_slice(&verify.stdout).unwrap();
|
|
|
|
|
assert_eq!(verify_payload["row_count"], 1);
|
|
|
|
|
assert_eq!(verify_payload["rows"][0]["p.name"], "Eve");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn change_can_resolve_uri_and_branch_from_config() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
write_config(&config, &local_yaml_config(&graph));
|
|
|
|
|
let mutation_file = temp.path().join("config-mutations.gq");
|
|
|
|
|
write_query_file(
|
|
|
|
|
&mutation_file,
|
|
|
|
|
r#"
|
|
|
|
|
query insert_person($name: String, $age: I32) {
|
|
|
|
|
insert Person { name: $name, age: $age }
|
|
|
|
|
}
|
|
|
|
|
"#,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("change")
|
|
|
|
|
.arg("--config")
|
|
|
|
|
.arg(&config)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(&mutation_file)
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Mia","age":30}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["branch"], "main");
|
|
|
|
|
assert_eq!(payload["affected_nodes"], 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_requires_name_for_multi_query_files() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(fixture("test.gq")),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(stderr.contains("multiple queries"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_supports_inline_query_string() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let repo = graph_path(temp.path());
|
|
|
|
|
init_graph(&repo);
|
|
|
|
|
load_fixture(&repo);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&repo)
|
|
|
|
|
.arg("-e")
|
|
|
|
|
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["query_name"], "find");
|
|
|
|
|
assert_eq!(payload["row_count"], 1);
|
|
|
|
|
assert_eq!(payload["rows"][0]["p.name"], "Alice");
|
|
|
|
|
}
|
|
|
|
|
|
feat(cli)!: remove legacy data-plane addressing (--target, positional http→remote, --as-on-served) (#238)
* feat(cli): --server accepts a literal URL (RFC-011 Decision 2)
`resolve_server_flag` now treats a `--server` value containing `://` as a literal
base URL (trailing slash trimmed; `--graph` appends `/graphs/<id>`), bypassing the
operator-config `servers:` registry; a bare name still resolves through the
registry. This is the replacement the upcoming `--uri http(s)://` deprecation
points at, and a small ergonomic win on its own (`--server https://host` with no
config entry). Token resolution for a literal-URL server falls to the legacy
OMNIGRAPH_BEARER_TOKEN chain, same as a positional URL today.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(cli): address the parity-matrix arms with global --store/--server flags
Prep for removing the positional-http→remote dispatch. The parity harness
addressed both arms with a positional graph right after the verb
(`omnigraph <verb> <addr> <args…>`), which only parses for top-level verbs —
for nested subcommands (`schema show`, `branch list`, …) the address landed in
the subcommand slot and BOTH arms failed identically, so the test passed
vacuously (matching exit codes, never comparing output).
Address both arms with the global flags instead — local `--store <graph>`
(embedded), remote `--server <url>` (served) — appended after the verb + args,
valid regardless of nesting. The previously-vacuous nested-verb parity checks
now actually compare embedded vs remote (and pass — parity holds), and the
remote arm no longer relies on the positional-URL dispatch that's about to be
removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli)!: --as on a served write is a hard error (was a silent no-op)
A served write resolves the actor server-side from the bearer token, so `--as`
could never set identity there — it was silently ignored. It now errors (in the
remote write factory, before any HTTP call), pointing the user at removing `--as`
or writing directly with `--store`. Reads don't carry `--as`, so this is
write-path only. BREAKING for any script that passed `--as` to a remote write
(it was a no-op, so behavior is unchanged except the now-explicit error).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli)!: a positional/--uri http(s):// URL no longer dispatches to a server
Remote graphs must be addressed with `--server <url>` (or a named server / a
profile binding one). A positional or `--uri` `http(s)://` URL on a data verb now
errors instead of silently routing to the remote HTTP client — the scheme no
longer carries transport semantics. The discriminator is `via_server`: a remote
URL produced by a server scope is fine; a remote URL from a positional/`--uri`
source is rejected (`reject_positional_remote` in both GraphClient factories).
Storage verbs are unaffected — they already reject remote URIs through
`resolve_local_graph` with the existing "direct (storage-native)" error.
Migrated the gh-host keyed-credential system test to `--server <url>` (the literal
URL still prefix-matches the operator server for token resolution). BREAKING:
scripts addressing a server by a bare URL must switch to `--server <url>`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli)!: remove the --target flag (use --store / --profile / --server)
Removes the legacy named-graph flag and threads its parameter out of the whole
resolver chain. `--target` resolved a graph name through `omnigraph.yaml`'s
`graphs:` map; its replacements (`--store <uri>`, `--profile <name>`,
`--server <name>`) all ship.
- Drops the 22 `target` clap fields + the `--cluster` exclusion that named it.
- Threads `target`/`cli_target` out of `resolve_uri`/`resolve_cli_graph`/
`resolve_local_graph`/`resolve_local_uri`/`resolve_storage_uri`/
`resolve_remote_bearer_token`/`apply_server_flag`/`execute_query_lint`/
`resolve_selected_graph`/`resolve_registry_selection_for_list`/
`execute_queries_{validate,list}`, the two `GraphClient` factories, and
`ScopeFlags`/`ResolvedScope`.
- Keeps the shared `OmnigraphConfig::resolve_target_uri` 3-arg (server boot uses
it); the CLI passes None for the explicit-target arm. The `cli.graph` default
(omnigraph.yaml bare-command fallback) is unchanged — its removal belongs to
the omnigraph.yaml excision.
- Operator/file aliases that bind a `graph` name still work: the name is now
resolved to a URI inline (a positional URI wins).
- Error messages and `--graph`/`--server`/`--store` help text no longer name
`--target`; the queries-list selection hint points at `cli.graph`.
BREAKING. Tests updated (named-target resolution rewritten onto `cli.graph`;
positional-URI tests unchanged). Full omnigraph-cli suite green (228).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): drop --target and positional-http addressing; --as-on-served is an error
Update the user docs for the legacy data-plane addressing removals:
- the CLI `--target` flag is gone — address graphs with a positional URI,
`--store`, `--profile`, or `--server <name|url>`;
- a positional `http(s)://` URI no longer dispatches to a server (use `--server`);
- `--as` on a served write is now rejected (was a silent no-op).
Touches cli/reference.md (addressing intro, capability table, error examples,
scopes), cli/index.md (the remote-read example → --server), operations/maintenance
+ policy, and the cluster docs' data-plane load guidance. The server's own
`--target` boot flag is unchanged (server.md untouched). Also fixes a pre-existing
broken maintenance link in search/indexes.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): --store is loudly exclusive with a positional URI / --server; test graphs→Served
Address two Greptile findings on the RFC-011 slices:
- Slice A (P1): `--store` combined with a positional URI silently dropped the URI
(`scope.rs` did `store.or(uri)`); `--store` + `--server` errored with a
misleading "positional URI" message. Now both combinations fail loudly with a
declared `--store is exclusive with a positional URI and --server` error.
- Slice B (P2): the `command_capability` unit test never exercised the one
Data→Served refinement (`graphs`); added the assertion so deleting that guard
can't pass silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 04:29:16 +03:00
|
|
|
#[test]
|
|
|
|
|
fn positional_http_uri_on_a_data_verb_is_rejected() {
|
|
|
|
|
// RFC-011: a positional/`--uri` http(s):// URL no longer dispatches to a
|
|
|
|
|
// remote server — that requires `--server <url>`.
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("query")
|
|
|
|
|
.arg("http://127.0.0.1:1")
|
|
|
|
|
.arg("-e")
|
|
|
|
|
.arg("query q() { match { $p: Person { } } return { $p } }"),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
assert!(
|
|
|
|
|
stderr.contains("must be addressed with `--server <url>`"),
|
|
|
|
|
"expected positional-remote rejection; got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn as_on_a_served_write_is_rejected() {
|
|
|
|
|
// RFC-011: a served write resolves the actor from the bearer token, so --as
|
|
|
|
|
// cannot set identity. It errors while building the remote client — before
|
|
|
|
|
// any HTTP call, so no server is needed.
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("mutate")
|
|
|
|
|
.arg("--server")
|
|
|
|
|
.arg("http://127.0.0.1:1")
|
|
|
|
|
.arg("--as")
|
|
|
|
|
.arg("act-nope")
|
|
|
|
|
.arg("-e")
|
|
|
|
|
.arg("query add($name: String) { insert Person { name: $name } }")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"X"}"#),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
assert!(
|
|
|
|
|
stderr.contains("`--as` is not allowed on a served write"),
|
|
|
|
|
"expected --as-served rejection; got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:16:51 +03:00
|
|
|
#[test]
|
|
|
|
|
fn change_supports_inline_query_string() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let repo = graph_path(temp.path());
|
|
|
|
|
init_graph(&repo);
|
|
|
|
|
load_fixture(&repo);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("change")
|
|
|
|
|
.arg(&repo)
|
|
|
|
|
.arg("--query-string")
|
|
|
|
|
.arg("query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Inline","age":42}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["query_name"], "add");
|
|
|
|
|
assert_eq!(payload["affected_nodes"], 1);
|
|
|
|
|
|
|
|
|
|
let verify = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&repo)
|
|
|
|
|
.arg("-e")
|
|
|
|
|
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name } }")
|
|
|
|
|
.arg("--params")
|
|
|
|
|
.arg(r#"{"name":"Inline"}"#)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let verify_payload: Value = serde_json::from_slice(&verify.stdout).unwrap();
|
|
|
|
|
assert_eq!(verify_payload["row_count"], 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_rejects_query_string_combined_with_query() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let repo = graph_path(temp.path());
|
|
|
|
|
init_graph(&repo);
|
|
|
|
|
load_fixture(&repo);
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("read")
|
|
|
|
|
.arg(&repo)
|
|
|
|
|
.arg("--query")
|
|
|
|
|
.arg(fixture("test.gq"))
|
|
|
|
|
.arg("-e")
|
|
|
|
|
.arg("query whatever() { match { $p: Person } return { $p.name } }"),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
stderr.contains("cannot be used") || stderr.contains("conflict"),
|
|
|
|
|
"expected clap conflict error, got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn read_rejects_empty_query_string() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let repo = graph_path(temp.path());
|
|
|
|
|
init_graph(&repo);
|
|
|
|
|
load_fixture(&repo);
|
|
|
|
|
|
|
|
|
|
let output = output_failure(cli().arg("read").arg(&repo).arg("-e").arg(""));
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
stderr.contains("must not be empty"),
|
|
|
|
|
"expected empty-string rejection, got: {stderr}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn branch_create_json_outputs_source_and_name() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["from"], "main");
|
|
|
|
|
assert_eq!(payload["name"], "feature");
|
|
|
|
|
assert_eq!(payload["uri"], graph.to_string_lossy().as_ref());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn branch_list_outputs_sorted_branches() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("zeta"),
|
|
|
|
|
);
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("alpha"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(cli().arg("branch").arg("list").arg("--uri").arg(&graph));
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
let lines = stdout
|
|
|
|
|
.lines()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|line| !line.is_empty())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
assert_eq!(lines, vec!["alpha", "main", "zeta"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn branch_delete_json_outputs_name_and_removes_branch() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("delete")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["name"], "feature");
|
|
|
|
|
assert_eq!(payload["uri"], graph.to_string_lossy().as_ref());
|
|
|
|
|
|
|
|
|
|
let listed = output_success(cli().arg("branch").arg("list").arg("--uri").arg(&graph));
|
|
|
|
|
let stdout = stdout_string(&listed);
|
|
|
|
|
let lines = stdout
|
|
|
|
|
.lines()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|line| !line.is_empty())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
assert_eq!(lines, vec!["main"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn branch_delete_rejects_main() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("delete")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("main"),
|
|
|
|
|
);
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(stderr.contains("cannot delete branch 'main'"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn branch_merge_defaults_target_to_main() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let feature_data = temp.path().join("feature.jsonl");
|
|
|
|
|
write_jsonl(
|
|
|
|
|
&feature_data,
|
|
|
|
|
r#"{"type":"Person","data":{"name":"Eve","age":29}}"#,
|
|
|
|
|
);
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("load")
|
|
|
|
|
.arg("--data")
|
|
|
|
|
.arg(&feature_data)
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
.arg("append")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let merge_output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("merge")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let merge_payload: Value = serde_json::from_slice(&merge_output.stdout).unwrap();
|
|
|
|
|
assert_eq!(merge_payload["source"], "feature");
|
|
|
|
|
assert_eq!(merge_payload["target"], "main");
|
|
|
|
|
assert_eq!(merge_payload["outcome"], "fast_forward");
|
|
|
|
|
|
|
|
|
|
let snapshot_output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("snapshot")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let snapshot: Value = serde_json::from_slice(&snapshot_output.stdout).unwrap();
|
|
|
|
|
let person_row_count = snapshot["tables"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|table| table["table_key"] == "node:Person")
|
|
|
|
|
.unwrap()["row_count"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(person_row_count, 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn branch_merge_supports_explicit_target() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("feature"),
|
|
|
|
|
);
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("create")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("--from")
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("experiment"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let feature_data = temp.path().join("feature-explicit.jsonl");
|
|
|
|
|
write_jsonl(
|
|
|
|
|
&feature_data,
|
|
|
|
|
r#"{"type":"Person","data":{"name":"Frank","age":41}}"#,
|
|
|
|
|
);
|
|
|
|
|
output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("load")
|
|
|
|
|
.arg("--data")
|
|
|
|
|
.arg(&feature_data)
|
|
|
|
|
.arg("--branch")
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
.arg("append")
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let merge_output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("merge")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("feature")
|
|
|
|
|
.arg("--into")
|
|
|
|
|
.arg("experiment")
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let merge_payload: Value = serde_json::from_slice(&merge_output.stdout).unwrap();
|
|
|
|
|
assert_eq!(merge_payload["target"], "experiment");
|
|
|
|
|
assert_eq!(merge_payload["outcome"], "fast_forward");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn snapshot_json_returns_manifest_version_and_tables() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_success(cli().arg("snapshot").arg(&graph).arg("--json"));
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["branch"], "main");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
payload["manifest_version"].as_u64().unwrap(),
|
|
|
|
|
manifest_dataset_version(&graph)
|
|
|
|
|
);
|
|
|
|
|
assert!(payload["tables"].as_array().unwrap().len() >= 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn snapshot_can_resolve_uri_from_config() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
write_config(&config, &local_yaml_config(&graph));
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("snapshot")
|
|
|
|
|
.arg("--config")
|
|
|
|
|
.arg(&config)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
assert_eq!(payload["branch"], "main");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn snapshot_human_output_includes_branch_and_table_summaries() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let output = output_success(cli().arg("snapshot").arg(&graph));
|
|
|
|
|
let stdout = stdout_string(&output);
|
|
|
|
|
|
|
|
|
|
assert!(stdout.contains("branch: main"));
|
|
|
|
|
assert!(stdout.contains("manifest_version:"));
|
|
|
|
|
assert!(stdout.contains("node:Person v"));
|
|
|
|
|
assert!(stdout.contains("edge:Knows v"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn commit_show_accepts_long_uri_flag() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let list = output_success(cli().arg("commit").arg("list").arg(&graph).arg("--json"));
|
|
|
|
|
let list_payload: Value = serde_json::from_slice(&list.stdout).unwrap();
|
|
|
|
|
let commit_id = list_payload["commits"][0]["graph_commit_id"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
let output = output_success(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("commit")
|
|
|
|
|
.arg("show")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg(&commit_id)
|
|
|
|
|
.arg("--json"),
|
|
|
|
|
);
|
|
|
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(payload["graph_commit_id"], commit_id);
|
|
|
|
|
assert!(payload["manifest_version"].as_u64().unwrap() >= 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cli_fails_for_missing_graph() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
|
|
|
|
|
let output = output_failure(cli().arg("snapshot").arg(&graph));
|
|
|
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
stderr.contains("_schema.pg")
|
|
|
|
|
|| stderr.contains("No such file")
|
|
|
|
|
|| stderr.contains("not found")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cli_fails_for_missing_schema_or_data_file() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
let missing_schema = temp.path().join("missing.pg");
|
|
|
|
|
let missing_data = temp.path().join("missing.jsonl");
|
|
|
|
|
|
|
|
|
|
let init_output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("init")
|
|
|
|
|
.arg("--schema")
|
|
|
|
|
.arg(&missing_schema)
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
String::from_utf8(init_output.stderr)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("No such file")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
let load_output = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("load")
|
|
|
|
|
.arg("--mode")
|
|
|
|
|
.arg("overwrite")
|
|
|
|
|
.arg("--data")
|
|
|
|
|
.arg(&missing_data)
|
|
|
|
|
.arg(&graph),
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
String::from_utf8(load_output.stderr)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("No such file")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cli_fails_for_invalid_merge_requests() {
|
|
|
|
|
let temp = tempdir().unwrap();
|
|
|
|
|
let graph = graph_path(temp.path());
|
|
|
|
|
init_graph(&graph);
|
|
|
|
|
load_fixture(&graph);
|
|
|
|
|
|
|
|
|
|
let missing_branch = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("merge")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("missing"),
|
|
|
|
|
);
|
|
|
|
|
let missing_branch_stderr = String::from_utf8(missing_branch.stderr).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
missing_branch_stderr.contains("missing")
|
|
|
|
|
|| missing_branch_stderr.contains("head commit")
|
|
|
|
|
|| missing_branch_stderr.contains("not found")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let same_branch = output_failure(
|
|
|
|
|
cli()
|
|
|
|
|
.arg("branch")
|
|
|
|
|
.arg("merge")
|
|
|
|
|
.arg("--uri")
|
|
|
|
|
.arg(&graph)
|
|
|
|
|
.arg("main")
|
|
|
|
|
.arg("--into")
|
|
|
|
|
.arg("main"),
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
String::from_utf8(same_branch.stderr)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("distinct source and target")
|
|
|
|
|
);
|
|
|
|
|
}
|