From 8e12aa8ca67f2d23c4673444bd374bddab95e9eb Mon Sep 17 00:00:00 2001 From: PYDuquesnoy Date: Fri, 7 Aug 2026 12:40:44 +0200 Subject: [PATCH] fix(namespace): omitted namespace resolves to the connection namespace, never a hardcoded USER (fixes #96) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Omitting the optional per-call namespace ran the call in USER regardless of the server's configured IRIS_NAMESPACE — silently, with plausible results from the wrong database. Root cause: a serde default `default_namespace() -> "USER"` duplicated across six files on ~25 param structs, applied before any connection logic could weigh in; the pool member's namespace (072) was overridden the same way. - namespace params become Option; the six default_namespace fns are deleted. The type change let the compiler enumerate every use site. - one shared resolve_namespace(param, connection_ns) in tools/mod.rs (observability.rs reuses it): explicit non-empty param wins, else the namespace of the connection THE CALL ACTUALLY USES — for server-routed calls that is the pool member's configured namespace, not the default connection's. - responses that echo namespace now echo the resolved value; tool descriptions updated ("defaults to the connection namespace"). - tests pin the new contract: omitted => None + resolves to the connection namespace, explicit => Some(...). Verified: cargo test --workspace 3866 passed / 0 failed; live stdio probe with IRIS_NAMESPACE=APP: iris_execute WRITE $NAMESPACE with no namespace arg -> "APP" (v1.0.0: "USER"), iris_query echoes namespace:"APP". Co-Authored-By: Claude Fable 5 --- .../iris-agentic-dev-core/src/tools/dict.rs | 49 +- crates/iris-agentic-dev-core/src/tools/doc.rs | 106 ++-- .../iris-agentic-dev-core/src/tools/info.rs | 94 ++-- crates/iris-agentic-dev-core/src/tools/mod.rs | 498 ++++++++++-------- .../src/tools/observability.rs | 11 +- crates/iris-agentic-dev-core/src/tools/scm.rs | 16 +- .../iris-agentic-dev-core/src/tools/search.rs | 44 +- .../tests/integration/test_handlers_live.rs | 36 +- .../integration/test_iris_doc_depth_live.rs | 2 +- .../tests/unit/test_compile_params.rs | 5 +- .../tests/unit/test_dict_unit.rs | 30 +- .../tests/unit/test_doc_unit2.rs | 3 +- .../tests/unit/test_info_unit.rs | 21 +- .../tests/unit/test_iris_doc_depth_unit.rs | 3 +- .../tests/unit/test_scm_unit.rs | 7 +- .../tests/unit/test_search_unit.rs | 8 +- 16 files changed, 500 insertions(+), 433 deletions(-) diff --git a/crates/iris-agentic-dev-core/src/tools/dict.rs b/crates/iris-agentic-dev-core/src/tools/dict.rs index 254b9f8..8301b1c 100644 --- a/crates/iris-agentic-dev-core/src/tools/dict.rs +++ b/crates/iris-agentic-dev-core/src/tools/dict.rs @@ -47,10 +47,6 @@ fn err_json(code: &str, msg: &str) -> Result String { - "USER".to_string() -} - // ── Tool 1: resolve_dynamic_dispatch ───────────────────────────────────────── #[derive(Debug, Deserialize, JsonSchema)] @@ -59,9 +55,9 @@ pub struct ResolveDynamicDispatchParams { pub method_name: String, /// Optional package prefix to restrict search (e.g. "EnsLib", "HS"). pub package_prefix: Option, - /// IRIS namespace. Defaults to "USER". - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Max candidates to return. Defaults to 50. pub limit: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. @@ -75,11 +71,12 @@ pub async fn handle_resolve_dynamic_dispatch( p: ResolveDynamicDispatchParams, cache: &MetadataCache, ) -> Result { + let namespace = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); let prefix = p.package_prefix.as_deref().unwrap_or(""); let limit = p.limit.unwrap_or(50); let cache_key = format!( "resolve_dynamic_dispatch:{}:{}:{}", - p.method_name, prefix, p.namespace + p.method_name, prefix, namespace ); if let Some(cached) = metadata_cache_get(cache, &cache_key) { return ok_json(cached); @@ -121,7 +118,7 @@ pub async fn handle_resolve_dynamic_dispatch( let code = lines.join("\n"); let output = iris - .execute_via_generator(&code, &p.namespace, client) + .execute_via_generator(&code, namespace, client) .await .map_err(|e| rmcp::ErrorData::internal_error(format!("execute failed: {e}"), None))?; let trimmed = output.trim(); @@ -146,7 +143,7 @@ pub async fn handle_resolve_dynamic_dispatch( c }) .collect(); - let r = serde_json::json!({"success":true,"method_name":p.method_name,"package_prefix":p.package_prefix,"namespace":p.namespace,"candidates":annotated,"candidate_count":n,"confidence":confidence,"truncated":n==limit}); + let r = serde_json::json!({"success":true,"method_name":p.method_name,"package_prefix":p.package_prefix,"namespace":namespace,"candidates":annotated,"candidate_count":n,"confidence":confidence,"truncated":n==limit}); metadata_cache_set(cache, cache_key, r.clone()); ok_json(r) } @@ -157,9 +154,9 @@ pub async fn handle_resolve_dynamic_dispatch( pub struct ExtractMessageMapParams { /// Fully qualified Ensemble class name (e.g. "HS.Flash.Router"). pub class_name: String, - /// IRIS namespace. Defaults to "USER". - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -204,20 +201,21 @@ pub async fn handle_extract_message_map_routing( p: ExtractMessageMapParams, cache: &MetadataCache, ) -> Result { - let cache_key = format!("extract_message_map:{}:{}", p.class_name, p.namespace); + let namespace = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); + let cache_key = format!("extract_message_map:{}:{}", p.class_name, namespace); if let Some(cached) = metadata_cache_get(cache, &cache_key) { return ok_json(cached); } // Check BPL/DTL first — these classes can't use the MessageMap generator path. - if let Some(flow) = detect_bpl_dtl_routing(iris, &p.class_name, &p.namespace, client).await { + if let Some(flow) = detect_bpl_dtl_routing(iris, &p.class_name, namespace, client).await { metadata_cache_set(cache, cache_key, flow.clone()); return ok_json(flow); } let code = build_message_map_code(&p.class_name); let output = iris - .execute_via_generator(&code, &p.namespace, client) + .execute_via_generator(&code, namespace, client) .await .map_err(|e| rmcp::ErrorData::internal_error(format!("execute failed: {e}"), None))?; let trimmed = output.trim(); @@ -257,7 +255,7 @@ pub async fn handle_extract_message_map_routing( let routes = inner["routes"].as_array().cloned().unwrap_or_default(); let route_count = routes.len(); - let r = serde_json::json!({"success":true,"class_name":p.class_name,"namespace":p.namespace,"has_message_map":has_mm,"routes":routes,"route_count":route_count}); + let r = serde_json::json!({"success":true,"class_name":p.class_name,"namespace":namespace,"has_message_map":has_mm,"routes":routes,"route_count":route_count}); metadata_cache_set(cache, cache_key, r.clone()); ok_json(r) } @@ -363,9 +361,9 @@ pub struct FindSubclassImplementationsParams { pub method_name: String, /// Base classes to expand — all descendants searched (e.g. ["Ens.BusinessProcess"]). pub base_classes: Vec, - /// IRIS namespace. Defaults to "USER". - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Max results. Defaults to 100. pub limit: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. @@ -409,13 +407,14 @@ pub async fn handle_find_subclass_implementations( return err_json("INVALID_PARAMS", "base_classes must not be empty"); } + let namespace = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); let mut sorted = p.base_classes.clone(); sorted.sort(); let cache_key = format!( "find_subclass:{}:{}:{}", p.method_name, sorted.join(","), - p.namespace + namespace ); if let Some(cached) = metadata_cache_get(cache, &cache_key) { return ok_json(cached); @@ -424,7 +423,7 @@ pub async fn handle_find_subclass_implementations( let limit = p.limit.unwrap_or(100); let expand_code = build_expand_hierarchy_code(&p.base_classes); let desc_raw = iris - .execute_via_generator(&expand_code, &p.namespace, client) + .execute_via_generator(&expand_code, namespace, client) .await .map_err(|e| { rmcp::ErrorData::internal_error(format!("hierarchy expansion failed: {e}"), None) @@ -437,7 +436,7 @@ pub async fn handle_find_subclass_implementations( .map(|s| s.to_string()) .collect(); if descendants.is_empty() { - let r = serde_json::json!({"success":true,"method_name":p.method_name,"base_classes":p.base_classes,"namespace":p.namespace,"implementations":[],"implementation_count":0,"confidence":0.0}); + let r = serde_json::json!({"success":true,"method_name":p.method_name,"base_classes":p.base_classes,"namespace":namespace,"implementations":[],"implementation_count":0,"confidence":0.0}); metadata_cache_set(cache, cache_key, r.clone()); return ok_json(r); } @@ -464,7 +463,7 @@ pub async fn handle_find_subclass_implementations( let code = lines.join("\n"); let output = iris - .execute_via_generator(&code, &p.namespace, client) + .execute_via_generator(&code, namespace, client) .await .map_err(|e| rmcp::ErrorData::internal_error(format!("method query failed: {e}"), None))?; let trimmed = output.trim(); @@ -483,7 +482,7 @@ pub async fn handle_find_subclass_implementations( i }) .collect(); - let r = serde_json::json!({"success":true,"method_name":p.method_name,"base_classes":p.base_classes,"namespace":p.namespace,"implementations":annotated,"implementation_count":n,"confidence":confidence}); + let r = serde_json::json!({"success":true,"method_name":p.method_name,"base_classes":p.base_classes,"namespace":namespace,"implementations":annotated,"implementation_count":n,"confidence":confidence}); metadata_cache_set(cache, cache_key, r.clone()); ok_json(r) } diff --git a/crates/iris-agentic-dev-core/src/tools/doc.rs b/crates/iris-agentic-dev-core/src/tools/doc.rs index 47ae85f..3ca5727 100644 --- a/crates/iris-agentic-dev-core/src/tools/doc.rs +++ b/crates/iris-agentic-dev-core/src/tools/doc.rs @@ -59,8 +59,9 @@ pub struct IrisDocParams { pub names: Vec, /// Source content (required for mode=put) pub content: Option, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Elicitation resume ID (from a prior elicitation_required response) pub elicitation_id: Option, /// User's answer to the elicitation question ("yes" or "no") @@ -146,9 +147,6 @@ where } } -fn default_namespace() -> String { - "USER".to_string() -} fn default_mode() -> String { "get".to_string() } @@ -303,17 +301,22 @@ pub async fn handle_iris_doc( ) } }; + // Resolve the effective namespace ONCE against the connection this call uses + // (pool member or default); sub-handlers receive the resolved value (issue #96). + let ns = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace).to_string(); match mode { - DocMode::Get => handle_get(iris, client, p).await, - DocMode::Put => handle_put(iris, client, p, elicitation_store, checkout_cache).await, - DocMode::Delete => handle_delete(iris, client, p).await, - DocMode::Head => handle_head(iris, client, p).await, - DocMode::Fragment => handle_fragment(iris, client, p).await, - DocMode::Compiled => handle_compiled(iris, client, p).await, - DocMode::List => handle_list(iris, client, p).await, - DocMode::Insert => handle_insert(iris, client, p, elicitation_store, checkout_cache).await, + DocMode::Get => handle_get(iris, client, p, &ns).await, + DocMode::Put => handle_put(iris, client, p, &ns, elicitation_store, checkout_cache).await, + DocMode::Delete => handle_delete(iris, client, p, &ns).await, + DocMode::Head => handle_head(iris, client, p, &ns).await, + DocMode::Fragment => handle_fragment(iris, client, p, &ns).await, + DocMode::Compiled => handle_compiled(iris, client, p, &ns).await, + DocMode::List => handle_list(iris, client, p, &ns).await, + DocMode::Insert => { + handle_insert(iris, client, p, &ns, elicitation_store, checkout_cache).await + } DocMode::DeleteLines => { - handle_delete_lines(iris, client, p, elicitation_store, checkout_cache).await + handle_delete_lines(iris, client, p, &ns, elicitation_store, checkout_cache).await } } } @@ -322,6 +325,7 @@ async fn handle_get( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, ) -> Result { // Batch get — Bug 19: fetch concurrently instead of sequentially. if !p.names.is_empty() { @@ -338,8 +342,7 @@ async fn handle_get( .unwrap_or_else(|_| client.clone()); let mut set = tokio::task::JoinSet::new(); for name in &p.names { - let url = - iris.versioned_ns_url(&p.namespace, &format!("/doc/{}", urlencoding::encode(name))); + let url = iris.versioned_ns_url(ns, &format!("/doc/{}", urlencoding::encode(name))); let username = iris.username.clone(); let password = iris.password.clone(); let name = name.clone(); @@ -380,10 +383,7 @@ async fn handle_get( Ok(n) => n, Err(r) => return Ok(r), }; - let url = iris.versioned_ns_url( - &p.namespace, - &format!("/doc/{}", urlencoding::encode(&name)), - ); + let url = iris.versioned_ns_url(ns, &format!("/doc/{}", urlencoding::encode(&name))); let resp = client .get(&url) .basic_auth(&iris.username, Some(&iris.password)) @@ -413,11 +413,11 @@ async fn handle_put( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, elicitation_store: &crate::elicitation::ElicitationStore, checkout_cache: &crate::elicitation::CheckoutCache, ) -> Result { let name = p.name.as_deref().unwrap_or(""); - let ns = &p.namespace; // Elicitation resume is handled centrally in handle_iris_doc before dispatch. @@ -728,14 +728,14 @@ async fn handle_delete( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, ) -> Result { // Batch delete if !p.names.is_empty() { let mut deleted = vec![]; let mut errors = vec![]; for name in &p.names { - let url = - iris.versioned_ns_url(&p.namespace, &format!("/doc/{}", urlencoding::encode(name))); + let url = iris.versioned_ns_url(ns, &format!("/doc/{}", urlencoding::encode(name))); match client .delete(&url) .basic_auth(&iris.username, Some(&iris.password)) @@ -771,10 +771,7 @@ async fn handle_delete( Ok(n) => n, Err(r) => return Ok(r), }; - let url = iris.versioned_ns_url( - &p.namespace, - &format!("/doc/{}", urlencoding::encode(&name)), - ); + let url = iris.versioned_ns_url(ns, &format!("/doc/{}", urlencoding::encode(&name))); let resp = client .delete(&url) .basic_auth(&iris.username, Some(&iris.password)) @@ -810,15 +807,13 @@ async fn handle_head( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, ) -> Result { let name = match require_name(&p, "head") { Ok(n) => n, Err(r) => return Ok(r), }; - let url = iris.versioned_ns_url( - &p.namespace, - &format!("/doc/{}", urlencoding::encode(&name)), - ); + let url = iris.versioned_ns_url(ns, &format!("/doc/{}", urlencoding::encode(&name))); let resp = client .head(&url) .basic_auth(&iris.username, Some(&iris.password)) @@ -1047,6 +1042,7 @@ async fn handle_insert( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, elicitation_store: &crate::elicitation::ElicitationStore, checkout_cache: &crate::elicitation::CheckoutCache, ) -> Result { @@ -1070,7 +1066,7 @@ async fn handle_insert( ); } - let existing = match fetch_doc_lines(iris, client, name, &p.namespace).await { + let existing = match fetch_doc_lines(iris, client, name, ns).await { Ok(Some(lines)) => lines, Ok(None) => return err_json("NOT_FOUND", &format!("Document not found: {name}")), Err(resp) => return Ok(resp), @@ -1106,7 +1102,7 @@ async fn handle_insert( client, name, &new_content, - &p.namespace, + ns, p.compile, true, // surgical insert never writes a full Storage block elicitation_store, @@ -1117,7 +1113,7 @@ async fn handle_insert( iris, client, name, - &p.namespace, + ns, result, serde_json::json!({ "edit": "insert", @@ -1133,6 +1129,7 @@ async fn handle_delete_lines( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, elicitation_store: &crate::elicitation::ElicitationStore, checkout_cache: &crate::elicitation::CheckoutCache, ) -> Result { @@ -1170,7 +1167,7 @@ async fn handle_delete_lines( } }; - let existing = match fetch_doc_lines(iris, client, name, &p.namespace).await { + let existing = match fetch_doc_lines(iris, client, name, ns).await { Ok(Some(lines)) => lines, Ok(None) => return err_json("NOT_FOUND", &format!("Document not found: {name}")), Err(resp) => return Ok(resp), @@ -1217,7 +1214,7 @@ async fn handle_delete_lines( client, name, &new_content, - &p.namespace, + ns, p.compile, true, // surgical delete never writes a full Storage block elicitation_store, @@ -1228,7 +1225,7 @@ async fn handle_delete_lines( iris, client, name, - &p.namespace, + ns, result, serde_json::json!({ "edit": "delete_lines", @@ -1303,6 +1300,7 @@ async fn handle_fragment( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, ) -> Result { let name = match require_name(&p, "fragment") { Ok(n) => n, @@ -1323,10 +1321,7 @@ async fn handle_fragment( ); } - let url = iris.versioned_ns_url( - &p.namespace, - &format!("/doc/{}", urlencoding::encode(&name)), - ); + let url = iris.versioned_ns_url(ns, &format!("/doc/{}", urlencoding::encode(&name))); let resp = client .get(&url) .basic_auth(&iris.username, Some(&iris.password)) @@ -1400,6 +1395,7 @@ async fn handle_compiled( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, ) -> Result { let name = match require_name(&p, "compiled") { Ok(n) => n, @@ -1440,10 +1436,7 @@ async fn handle_compiled( " Set rtn = ##class(%Library.Routine).%OpenId(\"{routine}.INT\")\n If rtn = \"\" {{ Write \"NOT_COMPILED\",$C(10) Quit }}\n Do rtn.Rewind()\n While 'rtn.AtEnd {{ Write rtn.ReadLine(),$C(10) }}\n Write \"DONE\",$C(10)" ); - let output = match iris - .execute_via_generator(&code, &p.namespace, client) - .await - { + let output = match iris.execute_via_generator(&code, ns, client).await { Ok(s) => s, Err(e) => return err_json("IRIS_EXECUTE_ERROR", &e.to_string()), }; @@ -1524,6 +1517,7 @@ async fn handle_list( iris: &IrisConnection, client: &reqwest::Client, p: IrisDocParams, + ns: &str, ) -> Result { let pattern = match p.pattern.as_deref() { Some(pat) => pat, @@ -1572,7 +1566,7 @@ async fn handle_list( let mut all_docs: Vec = Vec::new(); for cat in cats { - match fetch_docnames_for_cat(iris, client, &p.namespace, cat).await { + match fetch_docnames_for_cat(iris, client, ns, cat).await { Ok(docs) => all_docs.extend(docs), Err(e) => { return err_json( @@ -1611,7 +1605,7 @@ async fn handle_list( "documents": matched, "count": count, "truncated": truncated, - "namespace": p.namespace, + "namespace": ns, })) } @@ -1651,10 +1645,8 @@ pub async fn handle_iris_execute_method( let code = format!(" Set result = {call_expr}\n Write result,$C(10)"); - let output = match iris - .execute_via_generator(&code, &p.namespace, client) - .await - { + let namespace = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); + let output = match iris.execute_via_generator(&code, namespace, client).await { Ok(s) => s, Err(e) => { let msg = e.to_string(); @@ -1820,7 +1812,11 @@ mod tests { #[test] fn test_iris_doc_params_defaults() { let p: IrisDocParams = serde_json::from_str(r#"{}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "APP" + ); assert!(p.name.is_none()); assert!(p.names.is_empty()); assert!(p.content.is_none()); @@ -1920,7 +1916,11 @@ mod tests { fn test_iris_doc_params_namespace_override() { let p: IrisDocParams = serde_json::from_str(r#"{"name": "Foo.cls", "namespace": "MYNS"}"#).unwrap(); - assert_eq!(p.namespace, "MYNS"); + assert_eq!(p.namespace.as_deref(), Some("MYNS")); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "MYNS" + ); } #[test] diff --git a/crates/iris-agentic-dev-core/src/tools/info.rs b/crates/iris-agentic-dev-core/src/tools/info.rs index 04febae..a7731de 100644 --- a/crates/iris-agentic-dev-core/src/tools/info.rs +++ b/crates/iris-agentic-dev-core/src/tools/info.rs @@ -17,9 +17,6 @@ fn ok_json(v: serde_json::Value) -> Result Result { ok_json(serde_json::json!({"success": false, "error_code": code, "error": msg})) } -fn default_namespace() -> String { - "USER".to_string() -} fn default_limit() -> usize { 20 } @@ -34,8 +31,9 @@ pub struct InfoParams { pub doc_type: Option, /// Schema/cube name for what=sa_schema pub name: Option, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// If true, bypass the log store and return all results inline regardless of count. #[serde(default)] pub inline: bool, @@ -50,7 +48,7 @@ pub async fn handle_iris_info( p: InfoParams, log_store: Arc>, ) -> Result { - let ns = &p.namespace; + let ns = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); let url = match p.what.as_str() { "documents" => { // Bug 14: use versioned_ns_url so future API versions are used automatically. @@ -88,7 +86,7 @@ pub async fn handle_iris_info( } let body: serde_json::Value = resp.json().await.unwrap_or_default(); - let mut result_json = serde_json::json!({"success": true, "what": p.what, "namespace": p.namespace, "result": body["result"]}); + let mut result_json = serde_json::json!({"success": true, "what": p.what, "namespace": ns, "result": body["result"]}); // Progressive disclosure (027): for what=documents, truncate the document list. // The document names are in result["content"] — flatten to a top-level "documents" key. @@ -119,8 +117,9 @@ pub struct MacroParams { pub name: Option, #[serde(default)] pub args: Vec, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -131,10 +130,11 @@ pub async fn handle_iris_macro( client: &reqwest::Client, p: MacroParams, ) -> Result { + let ns = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); match p.action.as_str() { "list" => { // Bug 14: use versioned_ns_url instead of hardcoded /v1/. - let url = iris.versioned_ns_url(&p.namespace, "/docnames/INC"); + let url = iris.versioned_ns_url(ns, "/docnames/INC"); let resp = client .get(&url) .basic_auth(&iris.username, Some(&iris.password)) @@ -163,7 +163,7 @@ pub async fn handle_iris_macro( } action @ ("signature" | "location" | "definition" | "expand") => { let name = p.name.as_deref().unwrap_or(""); - let url = iris.versioned_ns_url(&p.namespace, "/action/getmacro"); + let url = iris.versioned_ns_url(ns, "/action/getmacro"); let arg_count = p.args.len(); let resp = client .post(&url) @@ -203,8 +203,9 @@ pub struct DebugParams { pub class_name: Option, #[serde(default = "default_limit")] pub limit: usize, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -215,7 +216,8 @@ pub async fn handle_iris_debug( _client: &reqwest::Client, p: DebugParams, ) -> Result { - let _query_url = iris.versioned_ns_url(&p.namespace, "/action/query"); + let ns = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); + let _query_url = iris.versioned_ns_url(ns, "/action/query"); match p.action.as_str() { "map_int" => { @@ -224,7 +226,7 @@ pub async fn handle_iris_debug( "set err=\"{}\" set routine=$piece($piece(err,\"^\",2),\".\",1) set offset=$piece(err,\"+\",2) set offset=$piece(offset,\"^\",1) write ##class(%Studio.Debugger).SourceLine(routine,+offset)", err.replace('"', "\\\"") ); - match iris.execute(&code, &p.namespace).await { + match iris.execute(&code, ns).await { Ok(output) => ok_json( serde_json::json!({"success": true, "error_string": err, "source_location": output.trim()}), ), @@ -247,7 +249,7 @@ pub async fn handle_iris_debug( } "capture" => { let code = "set err=$ZERROR write \"error:\"_err,! set loc=$ZPOSITION write \"position:\"_loc,!"; - match iris.execute(code, &p.namespace).await { + match iris.execute(code, ns).await { Ok(output) => { ok_json(serde_json::json!({"success": true, "capture": output.trim()})) } @@ -264,7 +266,7 @@ pub async fn handle_iris_debug( "set map=\"\" set line=1 do {{set int=##class(%Studio.Debugger).MapToINT(\"{cls}\",line,.intline) if int=\"\" quit set map=map_line_\"->\"_intline_\",\" set line=line+1 }} while 1 write map", cls = cls.replace('"', "\\\"") ); - match iris.execute(&code, &p.namespace).await { + match iris.execute(&code, ns).await { Ok(output) => ok_json( serde_json::json!({"success": true, "class": cls, "mapping": output.trim()}), ), @@ -300,8 +302,9 @@ pub struct GenerateParams { pub gen_type: String, /// Existing class name to generate tests for (gen_type=test only) pub class_name: Option, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -316,7 +319,7 @@ pub async fn handle_iris_generate( client: &reqwest::Client, p: GenerateParams, ) -> Result { - let ns = &p.namespace; + let ns = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); let query_url = iris.versioned_ns_url(ns, "/action/query"); match p.gen_type.as_str() { @@ -424,9 +427,9 @@ pub async fn handle_iris_generate( pub struct TableInfoParams { /// SQL table name in Schema.Table format (e.g. "SQLUser.MyTable" or "MyApp.Orders"). pub table: String, - /// IRIS namespace to query. Defaults to "USER". - #[serde(default = "crate::tools::default_namespace")] - pub namespace: String, + /// IRIS namespace to query. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Include approximate row count (runs SELECT COUNT(*) — may be slow on large tables). #[serde(default)] pub include_row_count: bool, @@ -440,6 +443,7 @@ pub async fn handle_iris_table_info( client: &reqwest::Client, p: TableInfoParams, ) -> Result { + let namespace = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); // Split "Schema.Table" → (schema, table). Tables with no dot use SQLUser schema. let (sql_schema, sql_table) = match p.table.find('.') { Some(idx) => (p.table[..idx].to_string(), p.table[idx + 1..].to_string()), @@ -470,7 +474,7 @@ if rs.%Next() {{ ); let output = iris - .execute_via_generator(&lookup_code, &p.namespace, client) + .execute_via_generator(&lookup_code, namespace, client) .await .map_err(|e| rmcp::ErrorData::internal_error(format!("execute failed: {e}"), None))?; @@ -480,9 +484,9 @@ if rs.%Next() {{ if output.trim() == "NOT_FOUND" { return crate::tools::ok_json(serde_json::json!({ "success": false, - "error": format!("Table '{}' not found in namespace '{}'", p.table, p.namespace), + "error": format!("Table '{}' not found in namespace '{}'", p.table, namespace), "table": p.table, - "namespace": p.namespace, + "namespace": namespace, })); } @@ -496,14 +500,14 @@ if rs.%Next() {{ "table": p.table, "type": "class_projection", "class": class_name, - "namespace": p.namespace, + "namespace": namespace, "data_global": if data_global.is_empty() { serde_json::Value::Null } else { data_global.into() }, "index_global": if index_global.is_empty() { serde_json::Value::Null } else { index_global.into() }, "accessible_from_embedded_python": true, }); if p.include_row_count { - let count = get_row_count(iris, client, &p.namespace, &sql_schema, &sql_table).await; + let count = get_row_count(iris, client, namespace, &sql_schema, &sql_table).await; obj["row_count"] = count; } obj @@ -516,7 +520,7 @@ if rs.%Next() {{ let mut obj = serde_json::json!({ "table": p.table, "type": "ddl_table", - "namespace": p.namespace, + "namespace": namespace, "data_global": data_global, "index_global": index_global, "id_counter_global": id_counter_global, @@ -524,7 +528,7 @@ if rs.%Next() {{ }); if p.include_row_count { - let count = get_row_count(iris, client, &p.namespace, &sql_schema, &sql_table).await; + let count = get_row_count(iris, client, namespace, &sql_schema, &sql_table).await; obj["row_count"] = count; } obj @@ -566,7 +570,11 @@ mod tests { #[test] fn test_info_params_defaults() { let p: InfoParams = serde_json::from_str(r#"{"what": "documents"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "APP" + ); assert!(p.doc_type.is_none()); assert!(p.name.is_none()); assert!(!p.inline); @@ -588,7 +596,11 @@ mod tests { #[test] fn test_macro_params_defaults() { let p: MacroParams = serde_json::from_str(r#"{"action": "list"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "APP" + ); assert!(p.name.is_none()); assert!(p.args.is_empty()); } @@ -606,7 +618,11 @@ mod tests { #[test] fn test_debug_params_defaults() { let p: DebugParams = serde_json::from_str(r#"{"action": "error_logs"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "APP" + ); assert!(p.error_string.is_none()); assert!(p.class_name.is_none()); assert!(p.limit > 0); @@ -617,7 +633,11 @@ mod tests { let p: GenerateParams = serde_json::from_str(r#"{"description": "A patient class"}"#).unwrap(); assert_eq!(p.gen_type, "class"); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "APP" + ); assert!(p.class_name.is_none()); } @@ -634,7 +654,11 @@ mod tests { #[test] fn test_table_info_params_defaults() { let p: TableInfoParams = serde_json::from_str(r#"{"table": "SQLUser.MyTable"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "APP" + ); assert!(!p.include_row_count); } diff --git a/crates/iris-agentic-dev-core/src/tools/mod.rs b/crates/iris-agentic-dev-core/src/tools/mod.rs index 7adb267..b84f550 100644 --- a/crates/iris-agentic-dev-core/src/tools/mod.rs +++ b/crates/iris-agentic-dev-core/src/tools/mod.rs @@ -668,8 +668,9 @@ pub struct CompileParams { pub target: String, #[serde(default = "default_flags")] pub flags: String, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, #[serde(default)] pub force_writable: bool, /// If true, bypass the log store and return all errors/warnings inline regardless of count. @@ -685,8 +686,9 @@ pub struct CompileParams { #[derive(Debug, Deserialize, JsonSchema)] pub struct TestParams { pub pattern: String, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, #[serde(default = "default_test_timeout")] pub timeout: u64, /// Set true to also measure line coverage inline (wraps iris_coverage mode=run) @@ -708,8 +710,9 @@ pub struct SymbolsParams { pub query: String, #[serde(default = "default_limit")] pub limit: usize, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -717,8 +720,9 @@ pub struct SymbolsParams { #[derive(Debug, Deserialize, JsonSchema)] pub struct IntrospectParams { pub class_name: String, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -731,16 +735,18 @@ pub struct DebugMapParams { pub offset: i64, #[serde(default)] pub error_string: String, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, } #[derive(Debug, Deserialize, JsonSchema)] pub struct GenerateClassParams { pub description: String, #[serde(default)] pub overwrite: bool, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -748,8 +754,9 @@ pub struct GenerateClassParams { #[derive(Debug, Deserialize, JsonSchema)] pub struct GenerateTestParams { pub class_name: String, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -819,13 +826,15 @@ fn default_symbols_local_limit() -> usize { } #[derive(Debug, Deserialize, JsonSchema)] pub struct CapturePacketParams { - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, } #[derive(Debug, Deserialize, JsonSchema)] pub struct ErrorLogsParams { - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, #[serde(default = "default_max_entries")] pub max_entries: usize, /// If true, bypass the log store and return all entries inline regardless of count. @@ -861,8 +870,9 @@ pub struct SourceMapParams { #[serde(default)] pub cls_text: Option, pub workspace_path: Option, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, } // 053-doc-depth #[derive(Debug, Deserialize, JsonSchema)] @@ -874,8 +884,9 @@ pub struct IrisExecuteMethodParams { /// Positional string arguments passed to the method #[serde(default)] pub args: Vec, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Route this call to a named registered IRIS instance. If omitted, uses the default connection. #[serde(default)] pub server: Option, @@ -884,8 +895,9 @@ pub struct IrisExecuteMethodParams { #[derive(Debug, Deserialize, JsonSchema)] pub struct ExecuteParams { pub code: String, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, #[serde(default = "default_execute_timeout")] pub timeout: u64, #[serde(default)] @@ -918,8 +930,9 @@ pub struct QueryParams { /// Query parameters as strings (e.g. ["Alice", "42"]) #[serde(default)] pub parameters: Vec, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// If true, bypass SQL safety validation. Use only for intentional administrative queries. /// Has no effect on production IRIS instances (where write tools are disabled). /// Ignored in mode="write" — see `force_ignored` in the response. @@ -946,8 +959,9 @@ pub struct ListContainersParams { #[derive(Debug, Deserialize, JsonSchema)] pub struct SelectContainerParams { pub name: String, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, #[serde(default = "default_username")] pub username: String, #[serde(default = "default_password")] @@ -964,8 +978,15 @@ pub struct StartSandboxParams { fn default_flags() -> String { "cuk".to_string() } -fn default_namespace() -> String { - "USER".to_string() +/// Resolve the effective namespace for a tool call: use `param` if non-empty, else +/// `connection_ns` (the configured namespace of the connection the call actually uses — +/// pool member or default). Callers with no connection in scope pass "USER" as +/// `connection_ns`, so USER is only ever the last-resort fallback (issue #96). +pub fn resolve_namespace<'a>(param: Option<&'a str>, connection_ns: &'a str) -> &'a str { + match param { + Some(s) if !s.is_empty() => s, + _ => connection_ns, + } } fn default_limit() -> usize { 20 @@ -1500,6 +1521,7 @@ async fn iris_query_explain( iris: &IrisConnection, client: &reqwest::Client, p: &QueryParams, + namespace: &str, ) -> Result { let first_word = p .query @@ -1517,7 +1539,7 @@ async fn iris_query_explain( ); } - let query_url = iris.versioned_ns_url(&p.namespace, "/action/query"); + let query_url = iris.versioned_ns_url(namespace, "/action/query"); let explain_sql = format!("EXPLAIN {}", p.query); let resp = client .post(&query_url) @@ -1567,6 +1589,7 @@ async fn iris_query_count( iris: &IrisConnection, client: &reqwest::Client, p: &QueryParams, + namespace: &str, ) -> Result { let table = p.table.as_deref(); let query = if p.query.trim().is_empty() { @@ -1582,7 +1605,7 @@ async fn iris_query_count( } let count_sql = build_count_query(table, query); - let query_url = iris.versioned_ns_url(&p.namespace, "/action/query"); + let query_url = iris.versioned_ns_url(namespace, "/action/query"); let resp = client .post(&query_url) .basic_auth(&iris.username, Some(&iris.password)) @@ -1624,6 +1647,7 @@ async fn iris_query_write( iris: &IrisConnection, client: &reqwest::Client, p: &QueryParams, + namespace: &str, ) -> Result { match validate_dml_sql(&p.query) { Err(ref reason) if reason == "EMPTY" => { @@ -1674,10 +1698,7 @@ If rs.%SQLCODE<0 {{ Write "ERROR:ROWS_CHECK_FAILED:"_rs.%Message Quit }} If rs.%Next() {{ Write "OK:"_rs.%GetData(1) }} Else {{ Write "OK:0" }}"#, count_sql = count_sql.replace('"', "\"\""), ); - match iris - .execute_via_generator(&code, &p.namespace, client) - .await - { + match iris.execute_via_generator(&code, namespace, client).await { Ok(out) => { let out = out.trim(); if let Some(msg) = out.strip_prefix("ERROR:ROWS_CHECK_FAILED:") { @@ -1717,10 +1738,7 @@ If rs.%SQLCODE<0 {{ Write "ERROR:SQL_ERROR:"_rs.%Message Quit }} Write "OK:"_rs.%ROWCOUNT"#, sql = p.query.replace('"', "\"\""), ); - match iris - .execute_via_generator(&code, &p.namespace, client) - .await - { + match iris.execute_via_generator(&code, namespace, client).await { Ok(out) => { let out = out.trim(); if let Some(msg) = out.strip_prefix("ERROR:SQL_ERROR:") { @@ -2802,8 +2820,9 @@ impl IrisTools { Parameters(p): Parameters, ) -> Result { let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace).to_string(); let (sm_server, policy) = self.active_server_manager_policy(); - let params_json = serde_json::json!({ "target": p.target, "namespace": p.namespace }); + let params_json = serde_json::json!({ "target": p.target, "namespace": namespace }); if let Err(gate) = crate::policy::gate::dispatch_gate( "iris_compile", sm_server.as_deref().unwrap_or(""), @@ -2861,7 +2880,7 @@ impl IrisTools { ) { return ok_json(gate); } - tracing::info!(namespace = %p.namespace, target = %p.target, "iris_compile"); + tracing::info!(namespace = %namespace, target = %p.target, "iris_compile"); // Capability gate: if atelier_rest is unavailable (docker_only or NoPWS build), // compile via docker exec immediately — no 52773 probe, no retry. @@ -2891,7 +2910,7 @@ impl IrisTools { p.target.replace('"', "\\\""), p.flags.replace('"', "\\\""), ); - let result = iris.execute(&code, &p.namespace).await; + let result = iris.execute(&code, &namespace).await; self.record_call("iris_compile", result.is_ok()); return match result { Ok(output) => { @@ -2900,7 +2919,7 @@ impl IrisTools { ok_json(serde_json::json!({ "success": success, "target": p.target, - "namespace": p.namespace, + "namespace": namespace, "method": "docker_exec", "output": trimmed, })) @@ -2954,7 +2973,7 @@ impl IrisTools { }); // Upload via Atelier PUT let put_url = iris.versioned_ns_url( - &p.namespace, + &namespace, &format!("/doc/{}?ignoreConflict=1", urlencoding::encode(&doc_name)), ); let lines: Vec<&str> = content.lines().collect(); @@ -2985,7 +3004,7 @@ impl IrisTools { // Compile via shared compile_document helper let local_src = p.target.clone(); let cr = iris - .compile_document(&doc_name, &p.namespace, &p.flags, client) + .compile_document(&doc_name, &namespace, &p.flags, client) .await .map_err(|e| McpError::internal_error(e.to_string(), None))?; let errors: Vec = cr @@ -3005,7 +3024,7 @@ impl IrisTools { "target": doc_name, "uploaded_from": local_src, "targets_compiled": 1, - "namespace": p.namespace, + "namespace": namespace, "errors": errors, "warnings": [], "console": console, @@ -3014,9 +3033,9 @@ impl IrisTools { } // Expand wildcards: resolve "MyApp.*.cls" to a list of matching class names. - // Bug 8: use p.namespace (not iris.namespace) and the correct /docnames/CLS endpoint. + // Bug 8: use namespace (not iris.namespace) and the correct /docnames/CLS endpoint. let targets: Vec = if p.target.contains('*') { - let list_url = iris.versioned_ns_url(&p.namespace, "/docnames/CLS"); + let list_url = iris.versioned_ns_url(&namespace, "/docnames/CLS"); match client .get(&list_url) .basic_auth(&iris.username, Some(&iris.password)) @@ -3055,15 +3074,15 @@ impl IrisTools { if p.force_writable { let code = format!( "do ##class(%Library.EnsembleMgr).EnableNamespace(\"{}\",1)", - p.namespace + namespace ); - let _ = iris.execute(&code, &p.namespace).await; + let _ = iris.execute(&code, &namespace).await; } // Atelier compile: POST with JSON array of document names (with extensions) // e.g. ["MyApp.Patient.cls", "MyApp.Utils.cls"] let compile_url = iris.versioned_ns_url( - &p.namespace, + &namespace, &format!("/action/compile?flags={}", urlencoding::encode(&p.flags)), ); @@ -3176,8 +3195,8 @@ impl IrisTools { // Write open hint for single non-wildcard successful compile let open_uri = if success && !p.target.contains('*') && targets.len() == 1 { - write_open_hint(&p.namespace, &p.target); - Some(format!("isfs://{}/{}", p.namespace, p.target)) + write_open_hint(&namespace, &p.target); + Some(format!("isfs://{}/{}", namespace, p.target)) } else { None }; @@ -3186,7 +3205,7 @@ impl IrisTools { "success": success, "target": p.target, "targets_compiled": targets.len(), - "namespace": p.namespace, + "namespace": namespace, "errors": errors, "warnings": warnings, "console": console, @@ -3225,7 +3244,6 @@ impl IrisTools { &self, Parameters(p): Parameters, ) -> Result { - tracing::info!(namespace = %p.namespace, pattern = %p.pattern, "iris_test"); let timeout = std::time::Duration::from_secs(p.timeout); // HTTP path only — docker exec path removed (#46: /noload/run assumed pre-loaded @@ -3233,12 +3251,14 @@ impl IrisTools { // errors; HTTP path with /verbose=1 is reliable and works with or without docker). let path_label = "http"; let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace).to_string(); + tracing::info!(namespace = %namespace, pattern = %p.pattern, "iris_test"); let client = self.http_client(); // US3: namespace existence check before running tests. let ns_check_code = format!( "write ##class(%SYS.Namespace).Exists(\"{}\")", - p.namespace.replace('"', "\\\"") + namespace.replace('"', "\\\"") ); let ns_exists = tokio::time::timeout( std::time::Duration::from_secs(10), @@ -3255,8 +3275,8 @@ impl IrisTools { return ok_json(serde_json::json!({ "success": false, "error_code": ERR_NAMESPACE_NOT_FOUND, - "error": format!("Namespace '{}' does not exist on this IRIS instance", p.namespace), - "namespace": p.namespace, + "error": format!("Namespace '{}' does not exist on this IRIS instance", namespace), + "namespace": namespace, })); } @@ -3322,7 +3342,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, }, test_path: None, target_pct: None, - namespace: Some(p.namespace.clone()), + namespace: Some(namespace.clone()), cobertura_path: None, }; // Ignore start errors — if monitor fails, coverage will return zeros/error @@ -3345,7 +3365,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, let run_output = if has_container { // Docker exec: full filesystem access, captures terminal output from RunTest - match tokio::time::timeout(timeout, iris.execute(&run_code, &p.namespace)).await { + match tokio::time::timeout(timeout, iris.execute(&run_code, &namespace)).await { Err(_) => { self.record_call("iris_test", false); return ok_json(serde_json::json!({ @@ -3358,7 +3378,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, // Docker exec unavailable — fall through to HTTP match tokio::time::timeout( timeout, - iris.execute_via_generator(&run_code, &p.namespace, client), + iris.execute_via_generator(&run_code, &namespace, client), ) .await { @@ -3379,7 +3399,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, // HTTP path: works for remote IRIS without docker match tokio::time::timeout( timeout, - iris.execute_via_generator(&run_code, &p.namespace, client), + iris.execute_via_generator(&run_code, &namespace, client), ) .await { @@ -3547,7 +3567,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, "error": "Pattern matched no test classes", "hint": hint, "pattern": p.pattern, - "namespace": p.namespace, + "namespace": namespace, "total": 0, "passed": 0, "failed": 0, @@ -3609,7 +3629,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, }, test_path: None, target_pct: p.coverage_target_pct, - namespace: Some(p.namespace.clone()), + namespace: Some(namespace.clone()), cobertura_path: None, }; let cov = coverage::handle_iris_coverage(&iris, client, &report_params).await; @@ -3621,7 +3641,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, package: None, test_path: None, target_pct: None, - namespace: Some(p.namespace.clone()), + namespace: Some(namespace.clone()), cobertura_path: None, }; let _ = coverage::handle_iris_coverage(&iris, client, &stop_params).await; @@ -3641,7 +3661,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, "path": path_label, "log_id": log_id, "pattern": p.pattern, - "namespace": p.namespace, + "namespace": namespace, "test_suites": test_suites, }); if let Some(cov) = coverage_result { @@ -3666,13 +3686,14 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, } else { self.get_iris_for_exec_with_client().await? }; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace).to_string(); // Diagnostic: the identity this connection will authenticate as, and whether a service // account is configured in the env at this instant. Surfaced in the response so account // routing is directly observable per call instead of inferred. let auth_user = iris.username.clone(); let svc_env = std::env::var("IRIS_SERVICE_USERNAME").unwrap_or_default(); let (sm_server, policy) = self.active_server_manager_policy(); - let params_json = serde_json::json!({ "namespace": p.namespace, "code": p.code }); + let params_json = serde_json::json!({ "namespace": &namespace, "code": p.code }); if let Err(gate) = crate::policy::gate::dispatch_gate( "iris_execute", sm_server.as_deref().unwrap_or(""), @@ -3730,7 +3751,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, ) { return ok_json(gate); } - tracing::info!(namespace = %p.namespace, translate_sql = p.translate_sql, use_session = p.use_session, "iris_execute"); + tracing::info!(namespace = %namespace, translate_sql = p.translate_sql, use_session = p.use_session, "iris_execute"); let client = exec_client.as_ref(); let timeout = std::time::Duration::from_secs(p.timeout); @@ -3777,7 +3798,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, // Try pure-HTTP execution first (write-compile-query via CodeMode=objectgenerator). let gen_result = tokio::time::timeout( timeout, - iris.execute_via_generator(code_to_run, &p.namespace, client), + iris.execute_via_generator(code_to_run, &namespace, client), ) .await; @@ -3809,7 +3830,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, "success": false, "error_code": err_code, "error": detail, - "namespace": p.namespace, + "namespace": namespace, "method": "http", "auth_user": auth_user, "service_account_env": svc_env, @@ -3824,7 +3845,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, let mut resp = serde_json::json!({ "success": !is_runtime_error, "output": trimmed, - "namespace": p.namespace, + "namespace": namespace, "method": "http", "auth_user": auth_user, "service_account_env": svc_env, @@ -3860,7 +3881,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, // Fallback: docker exec (requires IRIS_CONTAINER env var). let docker_result = - tokio::time::timeout(timeout, iris.execute(code_to_run, &p.namespace)).await; + tokio::time::timeout(timeout, iris.execute(code_to_run, &namespace)).await; match docker_result { Err(_) => { self.record_call("iris_execute", false); @@ -3903,7 +3924,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, let mut resp = serde_json::json!({ "success": !is_runtime_error, "output": trimmed, - "namespace": p.namespace, + "namespace": namespace, "method": "docker", }); if is_runtime_error { @@ -3938,7 +3959,8 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(p): Parameters, ) -> Result { let iris = self.resolve_server(p.server.as_deref()).await?; - tracing::info!(namespace = %p.namespace, "iris_doc"); + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); + tracing::info!(namespace = %namespace, "iris_doc"); let client = self.http_client(); let result = doc::handle_iris_doc( &iris, @@ -3961,13 +3983,17 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(p): Parameters, ) -> Result { let mode = p.mode.as_deref().unwrap_or("read"); - tracing::info!(namespace = %p.namespace, force = p.force, mode, "iris_query"); + // The gates below run before any connection is resolved, so log/audit the + // *requested* namespace here; each execution branch resolves the effective + // namespace against the connection it actually uses. + let requested_ns = p.namespace.as_deref().unwrap_or("(connection default)"); + tracing::info!(namespace = %requested_ns, force = p.force, mode, "iris_query"); // Policy gate (044 + 051): fires before role gate. let (sm_server_q, policy_q) = self.active_server_manager_policy(); { let params_json = - serde_json::json!({ "namespace": p.namespace, "mode": mode, "query": p.query }); + serde_json::json!({ "namespace": &p.namespace, "mode": mode, "query": p.query }); if let Err(gate) = crate::policy::gate::dispatch_gate( "iris_query", sm_server_q.as_deref().unwrap_or(""), @@ -4045,8 +4071,9 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, match mode { "explain" => { let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); - let result = iris_query_explain(&iris, client, &p).await; + let result = iris_query_explain(&iris, client, &p, namespace).await; self.record_call( "iris_query", result.as_ref().map(is_success).unwrap_or(false), @@ -4055,8 +4082,9 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, } "count" => { let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); - let result = iris_query_count(&iris, client, &p).await; + let result = iris_query_count(&iris, client, &p, namespace).await; self.record_call( "iris_query", result.as_ref().map(is_success).unwrap_or(false), @@ -4072,7 +4100,8 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, } else { self.get_iris_for_exec_with_client().await? }; - let result = iris_query_write(&iris, exec_client.as_ref(), &p).await; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); + let result = iris_query_write(&iris, exec_client.as_ref(), &p, namespace).await; self.record_call( "iris_query", result.as_ref().map(is_success).unwrap_or(false), @@ -4112,8 +4141,9 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, } let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace).to_string(); let client = self.http_client(); - let query_url = iris.versioned_ns_url(&p.namespace, "/action/query"); + let query_url = iris.versioned_ns_url(&namespace, "/action/query"); let resp = client .post(&query_url) .basic_auth(&iris.username, Some(&iris.password)) @@ -4147,7 +4177,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, let count = rows.len(); self.record_call("iris_query", true); ok_json( - serde_json::json!({"success": true, "rows": rows, "count": count, "namespace": p.namespace}), + serde_json::json!({"success": true, "rows": rows, "count": count, "namespace": namespace}), ) } @@ -4258,6 +4288,15 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(p): Parameters, ) -> Result { self.check_reload().await; + // This tool creates a NEW connection, so there is no existing connection to + // resolve against: explicit param wins, else the configured IRIS_NAMESPACE, + // else USER as the last resort. + let env_ns = std::env::var("IRIS_NAMESPACE") + .ok() + .filter(|s| !s.is_empty()); + let namespace = + resolve_namespace(p.namespace.as_deref(), env_ns.as_deref().unwrap_or("USER")) + .to_string(); let workspace_basename = String::new(); let containers = list_iris_containers(&workspace_basename).await; @@ -4287,7 +4326,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, let mut new_conn = crate::iris::connection::IrisConnection::new( &base_url, - &p.namespace, + &namespace, &p.username, &p.password, crate::iris::connection::DiscoverySource::Docker { @@ -4327,7 +4366,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, "container": p.name, "port_superserver": port_superserver, "port_web": port_web, - "namespace": p.namespace, + "namespace": namespace, "version": version, "write_tools_enabled": write_tools_enabled, })) @@ -4642,9 +4681,10 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(p): Parameters, ) -> Result { let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); let (sql, params) = translate_symbols_query(p.limit, &p.query); - match iris.query(&sql, params, &p.namespace, client).await { + match iris.query(&sql, params, namespace, client).await { Ok(resp) => ok_json(serde_json::json!({ "source": "iris_dictionary", "symbols": resp["result"]["content"], @@ -4721,19 +4761,20 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(p): Parameters, ) -> Result { let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); // Bug 15: use parameterized queries instead of manual string escaping. let methods = iris.query( "SELECT Name,FormalSpec,ReturnType FROM %Dictionary.CompiledMethod WHERE parent=? ORDER BY Name", vec![serde_json::Value::String(p.class_name.clone())], - &p.namespace, + namespace, client, ).await.unwrap_or_default(); let props = iris .query( "SELECT Name,Type FROM %Dictionary.CompiledProperty WHERE parent=? ORDER BY Name", vec![serde_json::Value::String(p.class_name.clone())], - &p.namespace, + namespace, client, ) .await @@ -4754,7 +4795,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, .collect::>(); // Detect BPL/DTL and add structured xdata_flow if present. - let xdata_flow = detect_xdata_flow(&iris, &p.class_name, &p.namespace, client).await; + let xdata_flow = detect_xdata_flow(&iris, &p.class_name, namespace, client).await; let mut resp = serde_json::json!({ "success": true, @@ -4783,16 +4824,14 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, } } let iris = self.get_iris_reloaded().await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); let code = format!( "Write ##class(%Studio.Debugger).SourceLine(\"{}\",{})", p.routine.replace('"', "\\\""), p.offset ); - match iris - .execute_via_generator(&code, &p.namespace, client) - .await - { + match iris.execute_via_generator(&code, namespace, client).await { Ok(raw) => { let (cls_name, cls_line) = parse_source_line(raw.trim()); ok_json( @@ -4812,8 +4851,9 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(_p): Parameters, ) -> Result { let iris = self.get_iris_reloaded().await?; + let namespace = resolve_namespace(_p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); - match iris.query("SELECT TOP 20 ErrorCode,ErrorText,TimeStamp FROM %SYSTEM.Error ORDER BY TimeStamp DESC", vec![], &_p.namespace, client).await { + match iris.query("SELECT TOP 20 ErrorCode,ErrorText,TimeStamp FROM %SYSTEM.Error ORDER BY TimeStamp DESC", vec![], namespace, client).await { Ok(resp) => ok_json(serde_json::json!({"success": true, "errors": resp["result"]["content"]})), Err(e) => { let msg = e.to_string(); @@ -4836,11 +4876,12 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(p): Parameters, ) -> Result { let iris = self.get_iris_reloaded().await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); // FR-012: cap max_entries to prevent runaway queries. let max_entries = p.max_entries.min(1000); let sql = format!("SELECT TOP {} ErrorCode,ErrorText,TimeStamp FROM %SYSTEM.Error ORDER BY TimeStamp DESC", max_entries); - match iris.query(&sql, vec![], &p.namespace, client).await { + match iris.query(&sql, vec![], namespace, client).await { Ok(resp) => { let mut result = serde_json::json!({"success": true, "logs": resp["result"]["content"]}); @@ -4881,6 +4922,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, Parameters(p): Parameters, ) -> Result { let iris = self.get_iris_reloaded().await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); let cls_name = p.cls_name.trim_end_matches(".cls"); // Build source map by querying %Studio.Debugger for each .INT method @@ -4888,10 +4930,7 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, "set cls=\"{}\" set rtn=$translate(cls,\".\",\".\") set map=\"{{\" set first=1 set method=\"\" for {{ set method=$order(^rIndex(rtn,method)) quit:method=\"\" set intline=$get(^rIndex(rtn,method)) if 'first {{ set map=map_\",\" }} set map=map_\"\\\"\"_method_\"\\\":\\\"\"_intline_\"\\\"\" set first=0 }} set map=map_\"}}\" write map", cls_name.replace('"', "\\\"") ); - match iris - .execute_via_generator(&code, &p.namespace, client) - .await - { + match iris.execute_via_generator(&code, namespace, client).await { Ok(output) => { let map: serde_json::Value = serde_json::from_str(output.trim()).unwrap_or(serde_json::json!({})); @@ -4939,13 +4978,14 @@ do ##class(%UnitTest.Manager).RunTest("{pattern}","{flags}","{token}")"#, extract_class_name(&class_text).unwrap_or_else(|| "Generated.Class".to_string()); if let Some(iris) = self.iris_arc().as_deref() { + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let _client = self.http_client(); let code = format!( "Set sc=$SYSTEM.OBJ.Compile(\"{}\",\"ck-d\") Write $System.Status.IsOK(sc)", class_name ); let compile_ok = iris - .execute(&code, &p.namespace) + .execute(&code, namespace) .await .map(|o| o.trim() == "1") .unwrap_or(false); @@ -4970,7 +5010,7 @@ Original: {}", fixed_name ); let ok2 = iris - .execute(&code2, &p.namespace) + .execute(&code2, namespace) .await .map(|o| o.trim() == "1") .unwrap_or(false); @@ -5006,12 +5046,13 @@ Original: {}", })?; let introspection_context = if let Some(iris) = self.iris_arc().as_deref() { + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); let client = self.http_client(); // FR-001/C1: use parameterized query to prevent SQL injection via class_name. iris.query( "SELECT Name,FormalSpec,ReturnType FROM %Dictionary.CompiledMethod WHERE parent=? ORDER BY Name", vec![serde_json::Value::String(p.class_name.clone())], - &p.namespace, + namespace, client, ) .await @@ -5747,10 +5788,11 @@ Methods: Parameters(p): Parameters, ) -> Result { let iris = self.resolve_server(p.server.as_deref()).await?; + let namespace = resolve_namespace(p.namespace.as_deref(), &iris.namespace); // Policy gate (044 + 051): check before role gate. let (sm_server_sc, policy_sc) = self.active_server_manager_policy(); { - let params_json = serde_json::json!({ "action": p.action, "namespace": p.namespace }); + let params_json = serde_json::json!({ "action": p.action, "namespace": namespace }); if let Err(gate) = crate::policy::gate::dispatch_gate( "iris_source_control", sm_server_sc.as_deref().unwrap_or(""), @@ -5992,16 +6034,14 @@ Methods: None => self.iris_arc(), }; let iris_opt = _iris_arc_hold.as_deref(); + let conn_ns = iris_opt.map(|i| i.namespace.as_str()).unwrap_or("USER"); + let ns_param = p.get("namespace").and_then(|v| v.as_str()); let result = match action { "status" => { interop::interop_production_status_impl( iris_opt, interop::ProductionStatusParams { - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), full_status: p.get("full").and_then(|v| v.as_bool()).unwrap_or(false), }, ) @@ -6015,11 +6055,7 @@ Methods: .get("production_name") .and_then(|v| v.as_str()) .map(|s| s.to_string()), - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), }, ) .await @@ -6032,11 +6068,7 @@ Methods: .get("production_name") .and_then(|v| v.as_str()) .map(|s| s.to_string()), - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), timeout: p.get("timeout").and_then(|v| v.as_u64()).unwrap_or(30) as u32, force: p.get("force").and_then(|v| v.as_bool()).unwrap_or(false), }, @@ -6047,11 +6079,7 @@ Methods: interop::interop_production_update_impl( iris_opt, interop::ProductionUpdateParams { - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), timeout: 30, force: false, }, @@ -6062,11 +6090,7 @@ Methods: interop::interop_production_needs_update_impl( iris_opt, interop::ProductionNeedsUpdateParams { - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), }, ) .await @@ -6075,11 +6099,7 @@ Methods: interop::interop_production_recover_impl( iris_opt, interop::ProductionRecoverParams { - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), }, ) .await @@ -6089,7 +6109,7 @@ Methods: iris_opt, &interop::ProductionAutostartParams { action: "get_autostart".into(), - namespace: p.get("namespace").and_then(|v| v.as_str()).unwrap_or("USER").to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), enabled: None, production: None, }, @@ -6100,7 +6120,7 @@ Methods: iris_opt, &interop::ProductionAutostartParams { action: "set_autostart".into(), - namespace: p.get("namespace").and_then(|v| v.as_str()).unwrap_or("USER").to_string(), + namespace: resolve_namespace(ns_param, conn_ns).to_string(), enabled: p.get("enabled").and_then(|v| v.as_bool()), production: p.get("production").and_then(|v| v.as_str()).map(|s| s.to_string()), }, @@ -6205,7 +6225,7 @@ Methods: "select" => { let params = SelectContainerParams { name: name.unwrap_or_default(), - namespace: default_namespace(), + namespace: None, username: default_username(), password: default_password(), }; @@ -6247,11 +6267,10 @@ Methods: .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let settings: std::collections::HashMap = p .get("settings") .and_then(|v| v.as_object()) @@ -6266,6 +6285,11 @@ Methods: Some(s) => Some(self.pool.get(Some(s))?), None => self.iris_arc(), }; + let conn_ns = _iris_arc_hold + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); + let namespace = resolve_namespace(requested_ns.as_deref(), conn_ns).to_string(); let result = interop::interop_production_item_impl( _iris_arc_hold.as_deref(), interop::ProductionItemParams { @@ -6298,11 +6322,17 @@ Methods: if message_id.is_empty() { return err_json("INVALID_PARAMS", "message_id is required"); } - let namespace = p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + let _iris_arc_hold: Option> = + match p.get("server").and_then(|v| v.as_str()) { + Some(s) => Some(self.pool.get(Some(s))?), + None => self.iris_arc(), + }; + let conn_ns = _iris_arc_hold + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); + let namespace = + resolve_namespace(p.get("namespace").and_then(|v| v.as_str()), conn_ns).to_string(); let max_bytes = p .get("max_bytes") .and_then(|v| v.as_u64()) @@ -6318,7 +6348,7 @@ Methods: .unwrap_or("block") .to_string(); let (sm_server, policy) = self.active_server_manager_policy(); - let params_json = serde_json::json!({ "namespace": namespace }); + let params_json = serde_json::json!({ "namespace": &namespace }); if let Err(gate) = crate::policy::gate::dispatch_gate( "iris_message_body", sm_server.as_deref().unwrap_or(""), @@ -6327,11 +6357,6 @@ Methods: ) { return ok_json(gate); } - let _iris_arc_hold: Option> = - match p.get("server").and_then(|v| v.as_str()) { - Some(s) => Some(self.pool.get(Some(s))?), - None => self.iris_arc(), - }; let result = interop::handle_iris_message_body( _iris_arc_hold.as_deref(), &interop::MessageBodyParams { @@ -6364,13 +6389,19 @@ Methods: .get("rule_name") .and_then(|v| v.as_str()) .map(|s| s.to_string()); - let namespace = p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + let _iris_arc_hold: Option> = + match p.get("server").and_then(|v| v.as_str()) { + Some(s) => Some(self.pool.get(Some(s))?), + None => self.iris_arc(), + }; + let conn_ns = _iris_arc_hold + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); + let namespace = + resolve_namespace(p.get("namespace").and_then(|v| v.as_str()), conn_ns).to_string(); let (sm_server, policy) = self.active_server_manager_policy(); - let params_json = serde_json::json!({ "namespace": namespace }); + let params_json = serde_json::json!({ "namespace": &namespace }); if let Err(gate) = crate::policy::gate::dispatch_gate( "iris_business_rule_info", sm_server.as_deref().unwrap_or(""), @@ -6379,11 +6410,6 @@ Methods: ) { return ok_json(gate); } - let _iris_arc_hold: Option> = - match p.get("server").and_then(|v| v.as_str()) { - Some(s) => Some(self.pool.get(Some(s))?), - None => self.iris_arc(), - }; let result = interop::handle_iris_business_rule_info( _iris_arc_hold.as_deref(), &interop::BusinessRuleInfoParams { @@ -6409,13 +6435,19 @@ Methods: .get("production") .and_then(|v| v.as_str()) .map(|s| s.to_string()); - let namespace = p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + let _iris_arc_hold: Option> = + match p.get("server").and_then(|v| v.as_str()) { + Some(s) => Some(self.pool.get(Some(s))?), + None => self.iris_arc(), + }; + let conn_ns = _iris_arc_hold + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); + let namespace = + resolve_namespace(p.get("namespace").and_then(|v| v.as_str()), conn_ns).to_string(); let (sm_server, policy) = self.active_server_manager_policy(); - let params_json = serde_json::json!({ "namespace": namespace }); + let params_json = serde_json::json!({ "namespace": &namespace }); if let Err(gate) = crate::policy::gate::dispatch_gate( "iris_production_diff", sm_server.as_deref().unwrap_or(""), @@ -6424,11 +6456,6 @@ Methods: ) { return ok_json(gate); } - let _iris_arc_hold: Option> = - match p.get("server").and_then(|v| v.as_str()) { - Some(s) => Some(self.pool.get(Some(s))?), - None => self.iris_arc(), - }; let result = interop::handle_iris_production_diff( _iris_arc_hold.as_deref(), &interop::ProductionDiffParams { @@ -6451,13 +6478,15 @@ Methods: &self, Parameters(p): Parameters, ) -> Result { - let namespace = p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + let iris_arc = self.iris_arc(); + let conn_ns = iris_arc + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); + let namespace = + resolve_namespace(p.get("namespace").and_then(|v| v.as_str()), conn_ns).to_string(); let result = interop::interop_credential_list_impl( - self.iris_arc().as_deref(), + iris_arc.as_deref(), interop::CredentialListParams { namespace }, ) .await; @@ -6473,8 +6502,13 @@ Methods: &self, Parameters(p): Parameters, ) -> Result { + let iris_arc = self.iris_arc(); + let conn_ns = iris_arc + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); let result = interop::interop_credential_manage_impl( - self.iris_arc().as_deref(), + iris_arc.as_deref(), interop::CredentialManageParams { action: p .get("action") @@ -6494,10 +6528,7 @@ Methods: .get("password") .and_then(|v| v.as_str()) .map(|s| s.to_string()), - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") + namespace: resolve_namespace(p.get("namespace").and_then(|v| v.as_str()), conn_ns) .to_string(), }, ) @@ -6516,8 +6547,13 @@ Methods: &self, Parameters(p): Parameters, ) -> Result { + let iris_arc = self.iris_arc(); + let conn_ns = iris_arc + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); let result = interop::interop_lookup_manage_impl( - self.iris_arc().as_deref(), + iris_arc.as_deref(), interop::LookupManageParams { action: p .get("action") @@ -6533,10 +6569,7 @@ Methods: .get("value") .and_then(|v| v.as_str()) .map(|s| s.to_string()), - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") + namespace: resolve_namespace(p.get("namespace").and_then(|v| v.as_str()), conn_ns) .to_string(), }, ) @@ -6552,8 +6585,13 @@ Methods: &self, Parameters(p): Parameters, ) -> Result { + let iris_arc = self.iris_arc(); + let conn_ns = iris_arc + .as_deref() + .map(|i| i.namespace.as_str()) + .unwrap_or("USER"); let result = interop::interop_lookup_transfer_impl( - self.iris_arc().as_deref(), + iris_arc.as_deref(), interop::LookupTransferParams { action: p .get("action") @@ -6566,10 +6604,7 @@ Methods: .unwrap_or("") .to_string(), xml: p.get("xml").and_then(|v| v.as_str()).map(|s| s.to_string()), - namespace: p - .get("namespace") - .and_then(|v| v.as_str()) - .unwrap_or("USER") + namespace: resolve_namespace(p.get("namespace").and_then(|v| v.as_str()), conn_ns) .to_string(), }, ) @@ -7260,14 +7295,14 @@ Methods: .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server_a = self.pool.get(Some(&server_a_name))?; let server_b = self.pool.get(Some(&server_b_name))?; + let namespace = resolve_namespace(requested_ns.as_deref(), &server_a.namespace).to_string(); let result = comparison_tools::compare_document_impl( comparison_tools::CompareDocumentParams { @@ -7291,11 +7326,10 @@ Methods: &self, Parameters(p): Parameters, ) -> Result { - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server_a_name = p .get("server_a") .and_then(|v| v.as_str()) @@ -7309,6 +7343,7 @@ Methods: let server_a = self.pool.get(Some(&server_a_name))?; let server_b = self.pool.get(Some(&server_b_name))?; + let namespace = resolve_namespace(requested_ns.as_deref(), &server_a.namespace).to_string(); let result = comparison_tools::compare_namespace_impl( comparison_tools::CompareNamespaceParams { @@ -7574,7 +7609,7 @@ Methods: } #[tool( - description = "Inspect the content of an IRIS stream object by OID. oid: the stream OID (integer string). namespace: optional namespace (default USER). server: optional registered instance name. Returns {content, type: 'text'|'binary', size, oid}. Skill: iris-agentic-dev.", + description = "Inspect the content of an IRIS stream object by OID. oid: the stream OID (integer string). namespace: optional namespace (defaults to the connection namespace, IRIS_NAMESPACE). server: optional registered instance name. Returns {content, type: 'text'|'binary', size, oid}. Skill: iris-agentic-dev.", annotations(read_only_hint = true) )] async fn stream_inspect( @@ -7586,16 +7621,16 @@ Methods: .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server = p .get("server") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let iris = self.resolve_server(server.as_deref()).await?; + let namespace = resolve_namespace(requested_ns.as_deref(), &iris.namespace).to_string(); let result = admin_tools::stream_inspect_impl(&iris, &self.client, &oid, &namespace).await; self.record_call("stream_inspect", result.is_ok()); result @@ -7647,23 +7682,23 @@ Methods: // ── 072-c: HL7 tools ────────────────────────────────────────────────────── #[tool( - description = "List available HL7 schemas on an IRIS/HealthShare instance. Returns HL7_NOT_AVAILABLE if EnsLib.HL7.Schema is absent. namespace: optional (default USER). server: optional registered instance name. Returns {schemas: [...], count: N}. Skill: iris-agentic-dev.", + description = "List available HL7 schemas on an IRIS/HealthShare instance. Returns HL7_NOT_AVAILABLE if EnsLib.HL7.Schema is absent. namespace: optional (defaults to the connection namespace, IRIS_NAMESPACE). server: optional registered instance name. Returns {schemas: [...], count: N}. Skill: iris-agentic-dev.", annotations(read_only_hint = true) )] async fn hl7_schema_list( &self, Parameters(p): Parameters, ) -> Result { - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server = p .get("server") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let iris = self.resolve_server(server.as_deref()).await?; + let namespace = resolve_namespace(requested_ns.as_deref(), &iris.namespace).to_string(); let result = admin_tools::hl7_schema_list_impl(&iris, &self.client, &namespace).await; self.record_call("hl7_schema_list", result.is_ok()); result @@ -7686,16 +7721,16 @@ Methods: .get("segment") .and_then(|v| v.as_str()) .map(|s| s.to_string()); - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server = p .get("server") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let iris = self.resolve_server(server.as_deref()).await?; + let namespace = resolve_namespace(requested_ns.as_deref(), &iris.namespace).to_string(); let result = admin_tools::hl7_schema_inspect_impl( &iris, &self.client, @@ -7724,16 +7759,16 @@ Methods: .unwrap_or("") .to_string(); let depth = p.get("depth").and_then(|v| v.as_u64()).unwrap_or(3) as u32; - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server = p .get("server") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let iris = self.resolve_server(server.as_deref()).await?; + let namespace = resolve_namespace(requested_ns.as_deref(), &iris.namespace).to_string(); let result = admin_tools::mermaid_class_impl(&iris, &self.client, &class, depth, &namespace).await; self.record_call("mermaid_class", result.is_ok()); @@ -7753,16 +7788,16 @@ Methods: .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server = p .get("server") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let iris = self.resolve_server(server.as_deref()).await?; + let namespace = resolve_namespace(requested_ns.as_deref(), &iris.namespace).to_string(); let result = admin_tools::mermaid_production_impl(&iris, &self.client, &production, &namespace) .await; @@ -7783,16 +7818,16 @@ Methods: .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let namespace = p + let requested_ns = p .get("namespace") .and_then(|v| v.as_str()) - .unwrap_or("USER") - .to_string(); + .map(|s| s.to_string()); let server = p .get("server") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let iris = self.resolve_server(server.as_deref()).await?; + let namespace = resolve_namespace(requested_ns.as_deref(), &iris.namespace).to_string(); let result = admin_tools::resolve_storage_impl(&iris, &self.client, &class, &namespace).await; self.record_call("resolve_storage", result.is_ok()); @@ -8666,20 +8701,23 @@ mod pure_fn_tests { #[test] fn test_compile_params_defaults() { let p: CompileParams = serde_json::from_str(r#"{"target": "Foo.Bar"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); assert_eq!(p.target, "Foo.Bar"); assert!(!p.force_writable); } #[test] fn test_test_params_defaults() { let p: TestParams = serde_json::from_str(r#"{"pattern": "MyTests.*"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); assert_eq!(p.pattern, "MyTests.*"); } #[test] fn test_execute_params_defaults() { let p: ExecuteParams = serde_json::from_str(r#"{"code": "Write 1"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); assert_eq!(p.code, "Write 1"); assert!(p.translate_sql, "translate_sql defaults to true"); assert!(!p.confirmed); @@ -8693,31 +8731,36 @@ mod pure_fn_tests { #[test] fn test_symbols_params_defaults() { let p: SymbolsParams = serde_json::from_str(r#"{"query": "Ens.*"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); } #[test] fn test_introspect_params_defaults() { let p: IntrospectParams = serde_json::from_str(r#"{"class_name": "Ens.Production"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); } #[test] fn test_generate_class_params_defaults() { let p: GenerateClassParams = serde_json::from_str(r#"{"description": "A simple class"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); assert!(!p.overwrite); } #[test] fn test_generate_test_params_defaults() { let p: GenerateTestParams = serde_json::from_str(r#"{"class_name": "Foo.Bar"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); assert_eq!(p.class_name, "Foo.Bar"); } #[test] fn test_query_params_defaults() { let p: QueryParams = serde_json::from_str(r#"{"query": "SELECT 1"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "APP"); assert!(p.parameters.is_empty()); } #[test] @@ -8933,7 +8976,8 @@ mod pure_fn_tests { fn test_test_params_namespace_override() { let p: TestParams = serde_json::from_str(r#"{"pattern": "T.*", "namespace": "MYNS"}"#).unwrap(); - assert_eq!(p.namespace, "MYNS"); + assert_eq!(p.namespace.as_deref(), Some("MYNS")); + assert_eq!(resolve_namespace(p.namespace.as_deref(), "APP"), "MYNS"); } #[test] diff --git a/crates/iris-agentic-dev-core/src/tools/observability.rs b/crates/iris-agentic-dev-core/src/tools/observability.rs index 128f6d1..90ee56c 100644 --- a/crates/iris-agentic-dev-core/src/tools/observability.rs +++ b/crates/iris-agentic-dev-core/src/tools/observability.rs @@ -63,13 +63,10 @@ pub fn glob_to_sql_like(pattern: &str) -> String { out } -/// Resolve the effective namespace: use `param` if non-empty, else `connection_ns`. -pub fn resolve_namespace<'a>(param: Option<&'a str>, connection_ns: &'a str) -> &'a str { - match param { - Some(s) if !s.is_empty() => s, - _ => connection_ns, - } -} +// Re-exported so existing `tools::observability::resolve_namespace` imports keep working. +// The shared definition lives in `crate::tools` (tools/mod.rs) since every tool module +// resolves per-call namespaces the same way (issue #96). +pub use crate::tools::resolve_namespace; // ── US1: view_locks ─────────────────────────────────────────────────────────── diff --git a/crates/iris-agentic-dev-core/src/tools/scm.rs b/crates/iris-agentic-dev-core/src/tools/scm.rs index bb2b041..79b2ff3 100644 --- a/crates/iris-agentic-dev-core/src/tools/scm.rs +++ b/crates/iris-agentic-dev-core/src/tools/scm.rs @@ -13,9 +13,6 @@ fn ok_json(v: serde_json::Value) -> Result Result { ok_json(serde_json::json!({"success": false, "error_code": code, "error": msg})) } -fn default_namespace() -> String { - "USER".to_string() -} /// Menu prefix used for source control actions. pub const SCM_MENU: &str = "%SourceMenu"; @@ -60,8 +57,9 @@ pub struct ScmParams { /// Elicitation resume answer pub answer: Option, pub elicitation_id: Option, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// Set to true to confirm write on a subject-role instance. Has no effect: source_control /// writes on subject instances are always hard-blocked regardless of confirm. #[serde(default)] @@ -120,7 +118,7 @@ pub async fn handle_iris_source_control( } else { raw_doc }; - let ns = &p.namespace; + let ns = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); // Handle elicitation resume if let (Some(eid), Some(answer)) = (&p.elicitation_id, &p.answer) { @@ -320,7 +318,7 @@ pub async fn handle_iris_source_control( ElicitationAction::ScmExecute, None, Some("%CheckOut".to_string()), - ns.clone(), + ns.to_string(), ); ok_json(serde_json::json!({ "success": false, @@ -377,7 +375,7 @@ pub async fn handle_iris_source_control( ElicitationAction::ScmExecute, None, Some(action_id.to_string()), - ns.clone(), + ns.to_string(), ); ok_json(serde_json::json!({ "success": false, "elicitation_required": true, "elicitation_id": eid, @@ -392,7 +390,7 @@ pub async fn handle_iris_source_control( ElicitationAction::ScmExecute, None, Some(action_id.to_string()), - ns.clone(), + ns.to_string(), ); ok_json(serde_json::json!({ "success": false, "elicitation_required": true, "elicitation_id": eid, diff --git a/crates/iris-agentic-dev-core/src/tools/search.rs b/crates/iris-agentic-dev-core/src/tools/search.rs index 8af6070..6817265 100644 --- a/crates/iris-agentic-dev-core/src/tools/search.rs +++ b/crates/iris-agentic-dev-core/src/tools/search.rs @@ -20,8 +20,9 @@ pub struct SearchParams { /// and times out server-side, so at least one scope must be provided. #[serde(default)] pub documents: Vec, - #[serde(default = "default_namespace")] - pub namespace: String, + /// IRIS namespace. Defaults to the connection namespace (IRIS_NAMESPACE). + #[serde(default)] + pub namespace: Option, /// If true, bypass the log store and return all results inline regardless of count. #[serde(default)] pub inline: bool, @@ -30,10 +31,6 @@ pub struct SearchParams { pub server: Option, } -fn default_namespace() -> String { - "USER".to_string() -} - fn ok_json(v: serde_json::Value) -> Result { Ok(rmcp::model::CallToolResult::success(vec![ rmcp::model::Content::text(v.to_string()), @@ -62,6 +59,7 @@ pub async fn handle_iris_search( p: SearchParams, log_store: Arc>, ) -> Result { + let namespace = crate::tools::resolve_namespace(p.namespace.as_deref(), &iris.namespace); let category = p.category.as_deref().unwrap_or("ALL"); // A scope is mandatory. Without one Atelier would grep the whole namespace and // time out server-side, returning nothing — an empty result that reads as @@ -95,7 +93,7 @@ pub async fn handle_iris_search( case_flag, ); - let sync_url = iris.versioned_ns_url(&p.namespace, &format!("/action/search?{}", query_string)); + let sync_url = iris.versioned_ns_url(namespace, &format!("/action/search?{}", query_string)); // Try the synchronous search first. Many IRIS servers answer `/action/search` // synchronously — even for broad wildcard scopes that take several seconds — @@ -131,19 +129,13 @@ pub async fn handle_iris_search( } let work_id = body["result"]["workId"].as_str().unwrap_or("").to_string(); poll_async_search( - iris, - client, - &work_id, - &p.namespace, - &p.query, - p.inline, - &log_store, + iris, client, &work_id, namespace, &p.query, p.inline, &log_store, ) .await } _ => { // Timeout or error — fall back to async POST - let post_url = iris.versioned_ns_url(&p.namespace, "/action/search"); + let post_url = iris.versioned_ns_url(namespace, "/action/search"); let post_body = serde_json::json!({ "query": p.query, "regex": p.regex, @@ -166,13 +158,7 @@ pub async fn handle_iris_search( let body: serde_json::Value = resp.json().await.unwrap_or_default(); if let Some(work_id) = body["result"]["workId"].as_str() { poll_async_search( - iris, - client, - work_id, - &p.namespace, - &p.query, - p.inline, - &log_store, + iris, client, work_id, namespace, &p.query, p.inline, &log_store, ) .await } else { @@ -315,13 +301,17 @@ mod tests { let result: Result = serde_json::from_str(r#"{"query":"x","namespace":"MYNS"}"#); assert!(result.is_ok()); - assert_eq!(result.unwrap().namespace, "MYNS"); + assert_eq!(result.unwrap().namespace.as_deref(), Some("MYNS")); } #[test] fn test_search_params_defaults() { let p: SearchParams = serde_json::from_str(r#"{"query": "Ens.*"}"#).unwrap(); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "APP" + ); assert!(!p.regex); assert!(!p.case_sensitive); assert!(p.category.is_none()); @@ -420,7 +410,11 @@ mod tests { assert!(p.case_sensitive); assert_eq!(p.category.as_deref(), Some("MAC")); assert_eq!(p.documents, vec!["App.*.cls"]); - assert_eq!(p.namespace, "PROD"); + assert_eq!(p.namespace.as_deref(), Some("PROD")); + assert_eq!( + crate::tools::resolve_namespace(p.namespace.as_deref(), "APP"), + "PROD" + ); assert!(p.inline); } diff --git a/crates/iris-agentic-dev-core/tests/integration/test_handlers_live.rs b/crates/iris-agentic-dev-core/tests/integration/test_handlers_live.rs index f5d4a11..feb8a00 100644 --- a/crates/iris-agentic-dev-core/tests/integration/test_handlers_live.rs +++ b/crates/iris-agentic-dev-core/tests/integration/test_handlers_live.rs @@ -296,7 +296,7 @@ fn test_handle_iris_info_namespace() { what: "namespace".to_string(), doc_type: None, name: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: false, server: None, }; @@ -327,7 +327,7 @@ fn test_handle_iris_info_documents() { what: "documents".to_string(), doc_type: Some("CLS".to_string()), name: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: true, server: None, }; @@ -360,7 +360,7 @@ fn test_handle_iris_search_basic() { case_sensitive: false, category: None, documents: vec![], - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: true, server: None, }; @@ -397,7 +397,7 @@ fn test_handle_iris_doc_get_object_cls() { name: Some("%Library.Object.cls".to_string()), names: vec![], content: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), elicitation_id: None, elicitation_answer: None, compile: false, @@ -452,7 +452,7 @@ fn test_handle_iris_doc_head_object_cls() { name: Some("%Library.Object.cls".to_string()), names: vec![], content: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), elicitation_id: None, elicitation_answer: None, compile: false, @@ -500,7 +500,7 @@ fn test_handle_iris_macro_list() { action: "list".to_string(), name: None, args: vec![], - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), server: None, }; let r = handle_iris_macro(&conn, &client, p).await; @@ -539,7 +539,7 @@ fn test_handle_iris_table_info() { } let p = TableInfoParams { table: "INFORMATION_SCHEMA.TABLES".to_string(), - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), include_row_count: false, server: None, }; @@ -606,7 +606,7 @@ fn test_handle_iris_info_metadata() { what: "metadata".to_string(), doc_type: None, name: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: false, server: None, }; @@ -636,7 +636,7 @@ fn test_handle_iris_info_invalid_what() { what: "invalid_value_xyz".to_string(), doc_type: None, name: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: false, server: None, }; @@ -711,7 +711,7 @@ fn test_handle_iris_doc_batch_get() { "%Library.RegisteredObject.cls".to_string(), ], content: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), elicitation_id: None, elicitation_answer: None, compile: false, @@ -760,7 +760,7 @@ fn test_handle_iris_search_regex() { case_sensitive: false, category: Some("CLS".to_string()), documents: vec![], - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: true, server: None, }; @@ -11857,7 +11857,7 @@ async fn test_search_sync_success_null_work_id() { case_sensitive: false, category: None, documents: vec![], - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: true, server: None, }; @@ -11914,7 +11914,7 @@ async fn test_search_sync_with_wiremock_null_work_id() { case_sensitive: false, category: None, documents: vec!["Test.*.cls".to_string()], - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: true, server: None, }; @@ -12001,7 +12001,7 @@ async fn test_search_async_poll_with_wiremock() { case_sensitive: false, category: None, documents: vec![], - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), inline: true, server: None, }; @@ -12056,7 +12056,7 @@ async fn test_doc_put_returns_200_with_status_errors() { name: Some("Test.Cls.cls".to_string()), names: vec![], content: Some("Class Test.Cls {}".to_string()), - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), elicitation_id: None, elicitation_answer: None, compile: false, @@ -12137,7 +12137,7 @@ async fn test_doc_put_compile_non_2xx_compile_request() { name: Some("Test.ConcurrentCompile.cls".to_string()), names: vec![], content: Some("Class Test.ConcurrentCompile {}".to_string()), - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), elicitation_id: None, elicitation_answer: None, compile: true, @@ -12207,7 +12207,7 @@ async fn test_doc_delete_non_2xx_non_404() { name: Some("Test.DeleteMe.cls".to_string()), names: vec![], content: None, - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), elicitation_id: None, elicitation_answer: None, compile: false, @@ -12275,7 +12275,7 @@ async fn test_doc_put_non_2xx_upload() { name: Some("Test.ReadOnly.cls".to_string()), names: vec![], content: Some("Class Test.ReadOnly {}".to_string()), - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), elicitation_id: None, elicitation_answer: None, compile: false, diff --git a/crates/iris-agentic-dev-core/tests/integration/test_iris_doc_depth_live.rs b/crates/iris-agentic-dev-core/tests/integration/test_iris_doc_depth_live.rs index 68e76a9..55fa405 100644 --- a/crates/iris-agentic-dev-core/tests/integration/test_iris_doc_depth_live.rs +++ b/crates/iris-agentic-dev-core/tests/integration/test_iris_doc_depth_live.rs @@ -64,7 +64,7 @@ fn execute_method_params(class: &str, method: &str, args: Vec<&str>) -> IrisExec class: class.to_string(), method: method.to_string(), args: args.iter().map(|s| s.to_string()).collect(), - namespace: "USER".to_string(), + namespace: Some("USER".to_string()), server: None, } } diff --git a/crates/iris-agentic-dev-core/tests/unit/test_compile_params.rs b/crates/iris-agentic-dev-core/tests/unit/test_compile_params.rs index 79bfe4c..4e299c6 100644 --- a/crates/iris-agentic-dev-core/tests/unit/test_compile_params.rs +++ b/crates/iris-agentic-dev-core/tests/unit/test_compile_params.rs @@ -9,7 +9,8 @@ mod tests { let p: CompileParams = serde_json::from_str(r#"{"target":"MyApp.Patient.cls"}"#).unwrap(); assert_eq!(p.target, "MyApp.Patient.cls"); assert_eq!(p.flags, "cuk"); - assert_eq!(p.namespace, "USER"); + // Omitted namespace stays None; resolution falls back to the connection namespace. + assert_eq!(p.namespace, None); assert!(!p.force_writable); } @@ -28,7 +29,7 @@ mod tests { r#"{"target":"HS.FHIR.*.cls","flags":"cuk","namespace":"HSLIB","force_writable":true}"#, ) .unwrap(); - assert_eq!(p.namespace, "HSLIB"); + assert_eq!(p.namespace.as_deref(), Some("HSLIB")); assert!(p.force_writable); } } diff --git a/crates/iris-agentic-dev-core/tests/unit/test_dict_unit.rs b/crates/iris-agentic-dev-core/tests/unit/test_dict_unit.rs index 6a9723a..deec669 100644 --- a/crates/iris-agentic-dev-core/tests/unit/test_dict_unit.rs +++ b/crates/iris-agentic-dev-core/tests/unit/test_dict_unit.rs @@ -163,7 +163,7 @@ mod tests { let p: ResolveDynamicDispatchParams = serde_json::from_str(r#"{"method_name": "Connect"}"#).unwrap(); assert_eq!(p.method_name, "Connect"); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); assert!(p.package_prefix.is_none()); assert!(p.limit.is_none()); } @@ -188,7 +188,7 @@ mod tests { fn test_resolve_params_custom_namespace() { let p: ResolveDynamicDispatchParams = serde_json::from_str(r#"{"method_name": "Connect", "namespace": "MYNS"}"#).unwrap(); - assert_eq!(p.namespace, "MYNS"); + assert_eq!(p.namespace.as_deref(), Some("MYNS")); } #[test] @@ -196,7 +196,7 @@ mod tests { let p: ExtractMessageMapParams = serde_json::from_str(r#"{"class_name": "HS.Flash.Router"}"#).unwrap(); assert_eq!(p.class_name, "HS.Flash.Router"); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); } #[test] @@ -204,7 +204,7 @@ mod tests { let p: ExtractMessageMapParams = serde_json::from_str(r#"{"class_name": "HS.Flash.Router", "namespace": "HSLIB"}"#) .unwrap(); - assert_eq!(p.namespace, "HSLIB"); + assert_eq!(p.namespace.as_deref(), Some("HSLIB")); } #[test] @@ -214,7 +214,7 @@ mod tests { ) .unwrap(); assert_eq!(p.method_name, "OnProcessInput"); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); assert!(p.limit.is_none()); assert_eq!(p.base_classes.len(), 1); } @@ -276,7 +276,7 @@ mod tests { .unwrap(); assert_eq!(p.method_name, "Execute"); assert_eq!(p.package_prefix.as_deref(), Some("HS")); - assert_eq!(p.namespace, "HSLIB"); + assert_eq!(p.namespace.as_deref(), Some("HSLIB")); assert_eq!(p.limit, Some(75)); } @@ -285,7 +285,7 @@ mod tests { let p: ExtractMessageMapParams = serde_json::from_str(r#"{"class_name": "MyApp.Router", "namespace": "PROD"}"#).unwrap(); assert_eq!(p.class_name, "MyApp.Router"); - assert_eq!(p.namespace, "PROD"); + assert_eq!(p.namespace.as_deref(), Some("PROD")); } #[test] @@ -295,7 +295,7 @@ mod tests { ) .unwrap(); assert_eq!(p.method_name, "OnProcessInput"); - assert_eq!(p.namespace, "ENSLIB"); + assert_eq!(p.namespace.as_deref(), Some("ENSLIB")); assert_eq!(p.limit, Some(100)); } @@ -405,16 +405,22 @@ mod tests { // ── Namespace defaults verification ─────────────────────────────────────── #[test] - fn test_all_params_have_user_default() { + fn test_all_params_default_to_connection_namespace() { + // Omitted namespace stays None; resolve_namespace falls back to the + // connection namespace (issue #96) — USER only when no connection exists. let r: ResolveDynamicDispatchParams = serde_json::from_str(r#"{"method_name": "M"}"#).unwrap(); - assert_eq!(r.namespace, "USER"); + assert_eq!(r.namespace, None); + assert_eq!( + iris_agentic_dev_core::tools::resolve_namespace(r.namespace.as_deref(), "APP"), + "APP" + ); let e: ExtractMessageMapParams = serde_json::from_str(r#"{"class_name": "C"}"#).unwrap(); - assert_eq!(e.namespace, "USER"); + assert_eq!(e.namespace, None); let f: FindSubclassImplementationsParams = serde_json::from_str(r#"{"method_name": "M", "base_classes": ["B"]}"#).unwrap(); - assert_eq!(f.namespace, "USER"); + assert_eq!(f.namespace, None); } } diff --git a/crates/iris-agentic-dev-core/tests/unit/test_doc_unit2.rs b/crates/iris-agentic-dev-core/tests/unit/test_doc_unit2.rs index 0a5dd1a..bc943ca 100644 --- a/crates/iris-agentic-dev-core/tests/unit/test_doc_unit2.rs +++ b/crates/iris-agentic-dev-core/tests/unit/test_doc_unit2.rs @@ -8,7 +8,8 @@ fn doc_params_defaults() { assert_eq!(p.name.as_deref(), Some("Foo.Bar.cls")); assert!(p.mode == "get"); assert!(!p.compile); - assert_eq!(p.namespace, "USER"); + // Omitted namespace stays None; resolution falls back to the connection namespace. + assert_eq!(p.namespace, None); } #[test] diff --git a/crates/iris-agentic-dev-core/tests/unit/test_info_unit.rs b/crates/iris-agentic-dev-core/tests/unit/test_info_unit.rs index eb0d4fc..e244218 100644 --- a/crates/iris-agentic-dev-core/tests/unit/test_info_unit.rs +++ b/crates/iris-agentic-dev-core/tests/unit/test_info_unit.rs @@ -9,7 +9,8 @@ fn info_params_defaults() { let p: InfoParams = serde_json::from_str(r#"{"what":"documents"}"#).unwrap(); assert_eq!(p.what, "documents"); assert!(p.doc_type.is_none()); - assert_eq!(p.namespace, "USER"); + // Omitted namespace stays None; resolution falls back to the connection namespace. + assert_eq!(p.namespace, None); assert!(!p.inline); } @@ -31,7 +32,7 @@ fn macro_params_defaults() { assert_eq!(p.action, "list"); assert!(p.name.is_none()); assert!(p.args.is_empty()); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); } #[test] @@ -47,7 +48,7 @@ fn generate_params_defaults_gen_type_class() { let p: GenerateParams = serde_json::from_str(r#"{"description":"a simple class"}"#).unwrap(); assert_eq!(p.gen_type, "class"); assert!(p.class_name.is_none()); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); } #[test] @@ -64,7 +65,7 @@ fn generate_params_test_type() { fn table_info_params_defaults() { let p: TableInfoParams = serde_json::from_str(r#"{"table":"SQLUser.MyTable"}"#).unwrap(); assert_eq!(p.table, "SQLUser.MyTable"); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); assert!(!p.include_row_count); } @@ -92,7 +93,7 @@ fn info_params_with_all_fields() { assert_eq!(p.what, "documents"); assert_eq!(p.doc_type.as_deref(), Some("MAC")); assert_eq!(p.name.as_deref(), Some("Foo")); - assert_eq!(p.namespace, "SYS"); + assert_eq!(p.namespace.as_deref(), Some("SYS")); assert!(p.inline); } @@ -186,7 +187,7 @@ fn macro_params_with_multiple_args() { #[test] fn macro_params_custom_namespace() { let p: MacroParams = serde_json::from_str(r#"{"action":"list","namespace":"SYS"}"#).unwrap(); - assert_eq!(p.namespace, "SYS"); + assert_eq!(p.namespace.as_deref(), Some("SYS")); } // ── DebugParams edge cases ─────────────────────────────────────────────────── @@ -229,7 +230,7 @@ fn debug_params_custom_limit() { fn debug_params_custom_namespace() { let p: DebugParams = serde_json::from_str(r#"{"action":"error_logs","namespace":"TEST"}"#).unwrap(); - assert_eq!(p.namespace, "TEST"); + assert_eq!(p.namespace.as_deref(), Some("TEST")); } // ── GenerateParams edge cases ──────────────────────────────────────────────── @@ -238,7 +239,7 @@ fn debug_params_custom_namespace() { fn generate_params_with_custom_namespace() { let p: GenerateParams = serde_json::from_str(r#"{"description":"a class","namespace":"CUSTOM"}"#).unwrap(); - assert_eq!(p.namespace, "CUSTOM"); + assert_eq!(p.namespace.as_deref(), Some("CUSTOM")); } #[test] @@ -248,7 +249,7 @@ fn generate_params_test_with_namespace() { ) .unwrap(); assert_eq!(p.gen_type, "test"); - assert_eq!(p.namespace, "SYS"); + assert_eq!(p.namespace.as_deref(), Some("SYS")); } // ── TableInfoParams edge cases ─────────────────────────────────────────────── @@ -263,7 +264,7 @@ fn table_info_params_with_dot_notation() { fn table_info_params_custom_namespace() { let p: TableInfoParams = serde_json::from_str(r#"{"table":"MyTable","namespace":"SYS"}"#).unwrap(); - assert_eq!(p.namespace, "SYS"); + assert_eq!(p.namespace.as_deref(), Some("SYS")); } #[test] diff --git a/crates/iris-agentic-dev-core/tests/unit/test_iris_doc_depth_unit.rs b/crates/iris-agentic-dev-core/tests/unit/test_iris_doc_depth_unit.rs index 36d17c7..3b291a4 100644 --- a/crates/iris-agentic-dev-core/tests/unit/test_iris_doc_depth_unit.rs +++ b/crates/iris-agentic-dev-core/tests/unit/test_iris_doc_depth_unit.rs @@ -235,7 +235,8 @@ fn test_execute_method_params_defaults() { let p: IrisExecuteMethodParams = serde_json::from_str(r#"{"class": "%Library.Integer", "method": "IsValid"}"#).unwrap(); assert!(p.args.is_empty()); - assert_eq!(p.namespace, "USER"); + // Omitted namespace stays None; resolution falls back to the connection namespace. + assert_eq!(p.namespace, None); } #[test] diff --git a/crates/iris-agentic-dev-core/tests/unit/test_scm_unit.rs b/crates/iris-agentic-dev-core/tests/unit/test_scm_unit.rs index 7525962..d70340f 100644 --- a/crates/iris-agentic-dev-core/tests/unit/test_scm_unit.rs +++ b/crates/iris-agentic-dev-core/tests/unit/test_scm_unit.rs @@ -42,7 +42,8 @@ fn test_scm_action_unknown() { fn test_scm_params_action_required() { let p: ScmParams = serde_json::from_str(r#"{"action": "status"}"#).unwrap(); assert_eq!(p.action, "status"); - assert_eq!(p.namespace, "USER"); // default_namespace + // Omitted namespace stays None; resolution falls back to the connection namespace. + assert_eq!(p.namespace, None); assert!(p.document.is_none()); assert!(p.action_id.is_none()); assert!(p.answer.is_none()); @@ -63,7 +64,7 @@ fn test_scm_params_full() { assert_eq!(p.action, "execute"); assert_eq!(p.document.as_deref(), Some("MyClass.cls")); assert_eq!(p.action_id.as_deref(), Some("CheckOut")); - assert_eq!(p.namespace, "MYNAMESPACE"); + assert_eq!(p.namespace.as_deref(), Some("MYNAMESPACE")); } #[test] @@ -79,7 +80,7 @@ fn test_scm_params_with_elicitation_fields() { assert_eq!(p.action, "execute"); assert_eq!(p.answer.as_deref(), Some("yes")); assert_eq!(p.elicitation_id.as_deref(), Some("eid-abc123")); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace, None); } #[test] diff --git a/crates/iris-agentic-dev-core/tests/unit/test_search_unit.rs b/crates/iris-agentic-dev-core/tests/unit/test_search_unit.rs index 1f12796..233a909 100644 --- a/crates/iris-agentic-dev-core/tests/unit/test_search_unit.rs +++ b/crates/iris-agentic-dev-core/tests/unit/test_search_unit.rs @@ -6,8 +6,8 @@ use iris_agentic_dev_core::tools::search::SearchParams; fn test_search_params_minimal() { let p: SearchParams = serde_json::from_str(r#"{"query": "test"}"#).unwrap(); assert_eq!(p.query, "test"); - // namespace defaults to "USER" - assert_eq!(p.namespace, "USER"); + // Omitted namespace stays None; resolution falls back to the connection namespace. + assert_eq!(p.namespace, None); // bool fields default to false assert!(!p.regex); assert!(!p.case_sensitive); @@ -30,7 +30,7 @@ fn test_search_params_full() { ) .unwrap(); assert_eq!(p.query, "Director"); - assert_eq!(p.namespace, "USER"); + assert_eq!(p.namespace.as_deref(), Some("USER")); assert!(p.regex); assert!(!p.case_sensitive); assert_eq!(p.category.as_deref(), Some("CLS")); @@ -49,7 +49,7 @@ fn test_search_params_case_sensitive_flag() { fn test_search_params_custom_namespace() { let p: SearchParams = serde_json::from_str(r#"{"query": "foo", "namespace": "IRISAPP"}"#).unwrap(); - assert_eq!(p.namespace, "IRISAPP"); + assert_eq!(p.namespace.as_deref(), Some("IRISAPP")); } #[test]