mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-07-12 03:12:11 +02:00
Merge branch 'main' into ragnorc/omnigraph-mcp-crate
Bring the MCP feature branch up to date with main (14 commits). One conflict — compiler/parser.rs: main's `NanoError` → `CompilerError` rename vs this branch's `@mcp` / per-param `@description` parser additions; resolved by keeping the new parsing under the renamed error type. The CLI `queries list` change (#280, surfacing `@description`/`@instruction`) auto-merged with this branch's `mcp_expose`/`tool_name` columns.
This commit is contained in:
commit
fbf455a250
110 changed files with 6396 additions and 2511 deletions
|
|
@ -13,7 +13,6 @@ use serde_json::Value;
|
|||
use serial_test::serial;
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
||||
mod support;
|
||||
use support::*;
|
||||
|
||||
|
|
@ -414,7 +413,7 @@ async fn cluster_boot_serves_applied_state() {
|
|||
assert!(server_policy.is_none());
|
||||
|
||||
let state =
|
||||
omnigraph_server::open_multi_graph_state(graphs, Vec::new(), None, config_path)
|
||||
omnigraph_server::open_multi_graph_state(graphs, Vec::new(), None, config_path, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let app = build_app(state);
|
||||
|
|
@ -424,7 +423,10 @@ async fn cluster_boot_serves_applied_state() {
|
|||
// GET /graphs refuses even in cluster mode.
|
||||
let (status, body) = json_response(
|
||||
&app,
|
||||
Request::builder().uri("/graphs").body(Body::empty()).unwrap(),
|
||||
Request::builder()
|
||||
.uri("/graphs")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::FORBIDDEN, "{body}");
|
||||
|
|
@ -460,6 +462,115 @@ async fn cluster_boot_serves_applied_state() {
|
|||
assert_eq!(status, StatusCode::OK, "{body}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cluster_boot_quarantines_graph_open_failures() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let schema = "\nnode Person {\n name: String @key\n}\n";
|
||||
let good_uri = temp.path().join("good.omni");
|
||||
Omnigraph::init(good_uri.to_string_lossy().as_ref(), schema)
|
||||
.await
|
||||
.unwrap();
|
||||
let bad_uri = temp.path().join("missing.omni");
|
||||
let server_policy = omnigraph_server::PolicySource::Inline(
|
||||
r#"
|
||||
version: 1
|
||||
kind: server
|
||||
groups:
|
||||
admins: [act-admin]
|
||||
rules:
|
||||
- id: admins-list-graphs
|
||||
allow:
|
||||
actors: { group: admins }
|
||||
actions: [graph_list]
|
||||
"#
|
||||
.to_string(),
|
||||
);
|
||||
let graphs = vec![
|
||||
omnigraph_server::GraphStartupConfig {
|
||||
graph_id: "broken".to_string(),
|
||||
uri: bad_uri.to_string_lossy().to_string(),
|
||||
policy: None,
|
||||
embedding: None,
|
||||
queries: stored_query_registry(&[]),
|
||||
},
|
||||
omnigraph_server::GraphStartupConfig {
|
||||
graph_id: "good".to_string(),
|
||||
uri: good_uri.to_string_lossy().to_string(),
|
||||
policy: None,
|
||||
embedding: None,
|
||||
queries: stored_query_registry(&[]),
|
||||
},
|
||||
];
|
||||
let strict_err = match omnigraph_server::open_multi_graph_state(
|
||||
graphs.clone(),
|
||||
vec![("act-admin".to_string(), "admin-token".to_string())],
|
||||
Some(&server_policy),
|
||||
temp.path().join("cluster.yaml"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("strict startup should reject a failed graph open"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert!(
|
||||
strict_err
|
||||
.to_string()
|
||||
.contains("strict multi-graph startup requires every graph to open"),
|
||||
"{strict_err}"
|
||||
);
|
||||
let state = omnigraph_server::open_multi_graph_state(
|
||||
graphs,
|
||||
vec![("act-admin".to_string(), "admin-token".to_string())],
|
||||
Some(&server_policy),
|
||||
temp.path().join("cluster.yaml"),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut ready: Vec<_> = state
|
||||
.routing()
|
||||
.registry
|
||||
.list()
|
||||
.iter()
|
||||
.map(|handle| handle.key.graph_id.as_str().to_string())
|
||||
.collect();
|
||||
ready.sort();
|
||||
assert_eq!(ready, vec!["good"]);
|
||||
let app = build_app(state);
|
||||
|
||||
let (status, body) = json_response(
|
||||
&app,
|
||||
Request::builder()
|
||||
.uri("/graphs")
|
||||
.header("authorization", "Bearer admin-token")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::OK, "{body}");
|
||||
assert_eq!(
|
||||
body["graphs"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|graph| graph["graph_id"].as_str().unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["good"]
|
||||
);
|
||||
|
||||
let (status, body) = json_response(
|
||||
&app,
|
||||
Request::builder()
|
||||
.uri("/graphs/broken/queries")
|
||||
.header("authorization", "Bearer admin-token")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NOT_FOUND, "{body}");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn cluster_boot_injects_embedding_provider_config() {
|
||||
|
|
@ -555,6 +666,7 @@ graphs:
|
|||
Vec::new(),
|
||||
server_policy.as_ref(),
|
||||
config_path,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -665,7 +777,10 @@ async fn cluster_boot_wires_policy_bindings_into_cedar_slots() {
|
|||
.unwrap();
|
||||
fs::write(
|
||||
temp.path().join("cluster.policy.yaml"),
|
||||
permit_all_policy_yaml(&["default"]).replace("protected_branches: [main]\n", "protected_branches: [main]\nkind: server\n"),
|
||||
permit_all_policy_yaml(&["default"]).replace(
|
||||
"protected_branches: [main]\n",
|
||||
"protected_branches: [main]\nkind: server\n",
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
|
|
@ -719,7 +834,7 @@ graphs:
|
|||
async fn cluster_boot_refusals() {
|
||||
// RFC-011 cluster-only: with no --cluster, boot refuses with the
|
||||
// cluster-required remedy.
|
||||
let err = omnigraph_server::load_server_settings(None, None, true)
|
||||
let err = omnigraph_server::load_server_settings(None, None, true, false)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("boots from a cluster"), "{err}");
|
||||
|
|
@ -729,7 +844,12 @@ async fn cluster_boot_refusals() {
|
|||
|
||||
// Tampered catalog blob refuses boot with the remedy.
|
||||
let blob_dir = dir.join("__cluster/resources/query/knowledge/find_person");
|
||||
let blob = fs::read_dir(&blob_dir).unwrap().next().unwrap().unwrap().path();
|
||||
let blob = fs::read_dir(&blob_dir)
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.path();
|
||||
fs::write(&blob, "tampered").unwrap();
|
||||
let err = cluster_settings(&dir).await.unwrap_err();
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use omnigraph_server::api::ReadRequest;
|
|||
use omnigraph_server::{AppState, build_app};
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
mod support;
|
||||
use support::*;
|
||||
|
||||
|
|
@ -137,6 +136,7 @@ async fn server_boots_cluster_from_bare_storage_uri_and_serves_query() {
|
|||
Some(&std::path::PathBuf::from(&root)),
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -153,6 +153,7 @@ async fn server_boots_cluster_from_bare_storage_uri_and_serves_query() {
|
|||
Vec::new(),
|
||||
server_policy.as_ref(),
|
||||
config_path,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -170,7 +171,9 @@ async fn server_boots_cluster_from_bare_storage_uri_and_serves_query() {
|
|||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(value["rows"][0]["p.name"], "Ada", "{value}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,6 +370,47 @@ async fn list_queries_requires_invoke_query() {
|
|||
assert!(names.contains(&"find_person"), "invoker sees the exposed query: {names:?}");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn list_queries_surfaces_query_description_and_instruction() {
|
||||
// E2e for the query-level `.gq` surface: `@description`/`@instruction` on
|
||||
// a stored query declaration are carried through to clients via the typed
|
||||
// `QueryCatalogEntry` fields over `GET /queries`. A query without them
|
||||
// omits both fields (serde `skip_serializing_if = "Option::is_none"`).
|
||||
let described = "query described($name: String) \
|
||||
@description(\"Find a person by exact name.\") \
|
||||
@instruction(\"Use for exact lookups; prefer search for fuzzy matches.\") \
|
||||
{ match { $p: Person { name: $name } } return { $p.age } }";
|
||||
let (_temp, app) = app_with_stored_queries(
|
||||
&[
|
||||
("described", described, true),
|
||||
("bare", "query bare() { match { $p: Person } return { $p.name } }", true),
|
||||
],
|
||||
&[("act-invoke", "t-invoke")],
|
||||
INVOKE_POLICY_YAML,
|
||||
)
|
||||
.await;
|
||||
let (status, body) = json_response(&app, get_request(&g("/queries"), "t-invoke")).await;
|
||||
assert_eq!(status, StatusCode::OK, "body: {body}");
|
||||
let entries = body["queries"].as_array().unwrap();
|
||||
|
||||
let described = entries.iter().find(|q| q["name"] == "described").unwrap();
|
||||
assert_eq!(
|
||||
described["description"], "Find a person by exact name.",
|
||||
"query @description surfaces over GET /queries: {described}"
|
||||
);
|
||||
assert_eq!(
|
||||
described["instruction"],
|
||||
"Use for exact lookups; prefer search for fuzzy matches.",
|
||||
"query @instruction surfaces over GET /queries: {described}"
|
||||
);
|
||||
|
||||
let bare = entries.iter().find(|q| q["name"] == "bare").unwrap();
|
||||
assert!(
|
||||
bare.get("description").is_none() && bare.get("instruction").is_none(),
|
||||
"a query without the annotations omits both fields: {bare}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn list_queries_is_empty_when_no_registry() {
|
||||
let (_temp, app) = app_for_loaded_graph_with_auth("demo-token").await;
|
||||
|
|
|
|||
|
|
@ -15,15 +15,12 @@ use omnigraph::db::{Omnigraph, ReadTarget};
|
|||
use omnigraph::error::OmniError;
|
||||
use omnigraph::loader::{LoadMode, load_jsonl};
|
||||
use omnigraph_policy::{PolicyChecker, PolicyEngine};
|
||||
use omnigraph_server::api::{
|
||||
BranchCreateRequest, BranchMergeRequest, ChangeRequest, ReadRequest,
|
||||
};
|
||||
use omnigraph_server::api::{BranchCreateRequest, BranchMergeRequest, ChangeRequest, ReadRequest};
|
||||
use omnigraph_server::queries::{QueryRegistry, RegistrySpec};
|
||||
use omnigraph_server::{AppState, build_app};
|
||||
use serde_json::{Value, json};
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
||||
pub const MUTATION_QUERIES: &str = r#"
|
||||
query insert_person($name: String, $age: I32) {
|
||||
insert Person { name: $name, age: $age }
|
||||
|
|
@ -1212,6 +1209,8 @@ graphs:
|
|||
temp
|
||||
}
|
||||
|
||||
pub async fn cluster_settings(dir: &Path) -> color_eyre::eyre::Result<omnigraph_server::ServerConfig> {
|
||||
omnigraph_server::load_server_settings(Some(&dir.to_path_buf()), None, true).await
|
||||
pub async fn cluster_settings(
|
||||
dir: &Path,
|
||||
) -> color_eyre::eyre::Result<omnigraph_server::ServerConfig> {
|
||||
omnigraph_server::load_server_settings(Some(&dir.to_path_buf()), None, true, false).await
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue