diff --git a/core/src/ten_manager/src/cmd/cmd_check/cmd_check_graph.rs b/core/src/ten_manager/src/cmd/cmd_check/cmd_check_graph.rs index f2ea1646ec..7715dc7bff 100644 --- a/core/src/ten_manager/src/cmd/cmd_check/cmd_check_graph.rs +++ b/core/src/ten_manager/src/cmd/cmd_check/cmd_check_graph.rs @@ -132,8 +132,9 @@ fn get_graphs_to_be_checked(command: &CheckGraphCommand) -> Result> { let mut graphs_to_be_checked: Vec = Vec::new(); if let Some(graph_str) = &command.graph { - let graph: Graph = Graph::from_str(graph_str) - .with_context(|| "The graph json string is invalid")?; + let graph: Graph = Graph::from_str(graph_str).map_err(|e| { + anyhow::anyhow!("Failed to parse graph string, {}", e) + })?; graphs_to_be_checked.push(graph); } else { let first_app_path = path::Path::new(&command.app[0]); diff --git a/core/src/ten_manager/tests/cmd_check_graph.rs b/core/src/ten_manager/tests/cmd_check_graph.rs index 674101e4a2..5c8dbc38dd 100644 --- a/core/src/ten_manager/tests/cmd_check_graph.rs +++ b/core/src/ten_manager/tests/cmd_check_graph.rs @@ -103,8 +103,7 @@ async fn test_cmd_check_app_in_graph_cannot_be_localhost() { eprintln!("{:?}", result); let msg = result.err().unwrap().to_string(); - assert!(msg - .contains("the app uri should be some string other than 'localhost'")); + assert!(msg.contains("'localhost' is not allowed in graph definition")); } #[actix_rt::test] @@ -179,3 +178,57 @@ async fn test_cmd_check_unique_extension_in_connections() { assert!(result.is_err()); eprintln!("{:?}", result); } + +#[actix_rt::test] +async fn test_cmd_check_single_app_node_cannot_be_localhost() { + let tman_config = TmanConfig::default(); + let command = CheckGraphCommand { + app: vec!["tests/test_data/cmd_check_single_app_node_cannot_be_localhost".to_string()], + graph: Some( + include_str!( + "test_data/cmd_check_single_app_node_cannot_be_localhost/start_graph.json" + ) + .to_string(), + ), + predefined_graph_name: None, + }; + + let result = ten_manager::cmd::cmd_check::cmd_check_graph::execute_cmd( + &tman_config, + command, + ) + .await; + + assert!(result.is_err()); + eprintln!("{:?}", result); + + let msg = result.err().unwrap().to_string(); + assert!(msg.contains("'localhost' is not allowed in graph definition")); +} + +#[actix_rt::test] +async fn test_cmd_check_multi_apps_node_cannot_be_localhost() { + let tman_config = TmanConfig::default(); + let command = CheckGraphCommand { + app: vec!["tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost".to_string()], + graph: Some( + include_str!( + "test_data/cmd_check_multi_apps_node_cannot_be_localhost/start_graph.json" + ) + .to_string(), + ), + predefined_graph_name: None, + }; + + let result = ten_manager::cmd::cmd_check::cmd_check_graph::execute_cmd( + &tman_config, + command, + ) + .await; + + assert!(result.is_err()); + eprintln!("{:?}", result); + + let msg = result.err().unwrap().to_string(); + assert!(msg.contains("'localhost' is not allowed in graph definition")); +} diff --git a/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/manifest.json b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/manifest.json new file mode 100644 index 0000000000..a307263751 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/manifest.json @@ -0,0 +1,22 @@ +{ + "type": "app", + "name": "cmd_check_single_app", + "version": "0.1.0", + "dependencies": [ + { + "type": "extension", + "name": "addon_a", + "version": "0.1.0" + }, + { + "type": "extension", + "name": "addon_b", + "version": "0.1.0" + } + ], + "package": { + "include": [ + "**" + ] + } +} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/property.json b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/property.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/property.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/start_graph.json b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/start_graph.json new file mode 100644 index 0000000000..ed66b62152 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/start_graph.json @@ -0,0 +1,37 @@ +{ + "type": "start_graph", + "nodes": [ + { + "type": "extension", + "name": "ext_a", + "addon": "addon_a", + "extension_group": "some_group", + "app": "http://localhost:8000" + }, + { + "type": "extension", + "name": "ext_b", + "addon": "addon_b", + "extension_group": "some_group", + "app": "localhost" + } + ], + "connections": [ + { + "extension_group": "some_group", + "extension": "ext_a", + "app": "http://localhost:8000", + "cmd": [ + { + "name": "cmd_1", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_b" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/ten_packages/extension/addon_a/manifest.json b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/ten_packages/extension/addon_a/manifest.json new file mode 100644 index 0000000000..05434a5648 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/ten_packages/extension/addon_a/manifest.json @@ -0,0 +1,18 @@ +{ + "type": "extension", + "name": "addon_a", + "version": "0.1.0", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_go", + "version": "0.1.0" + } + ], + "package": { + "include": [ + "**" + ] + }, + "api": {} +} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/ten_packages/extension/addon_b/manifest.json b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/ten_packages/extension/addon_b/manifest.json new file mode 100644 index 0000000000..9821e29f4d --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_multi_apps_node_cannot_be_localhost/ten_packages/extension/addon_b/manifest.json @@ -0,0 +1,18 @@ +{ + "type": "extension", + "name": "addon_b", + "version": "0.1.0", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_go", + "version": "0.1.0" + } + ], + "package": { + "include": [ + "**" + ] + }, + "api": {} +} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/manifest.json b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/manifest.json new file mode 100644 index 0000000000..a307263751 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/manifest.json @@ -0,0 +1,22 @@ +{ + "type": "app", + "name": "cmd_check_single_app", + "version": "0.1.0", + "dependencies": [ + { + "type": "extension", + "name": "addon_a", + "version": "0.1.0" + }, + { + "type": "extension", + "name": "addon_b", + "version": "0.1.0" + } + ], + "package": { + "include": [ + "**" + ] + } +} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/property.json b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/property.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/property.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/start_graph.json b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/start_graph.json new file mode 100644 index 0000000000..cd32b7a8f7 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/start_graph.json @@ -0,0 +1,36 @@ +{ + "type": "start_graph", + "nodes": [ + { + "type": "extension", + "name": "ext_a", + "addon": "addon_a", + "extension_group": "some_group", + "app": "localhost" + }, + { + "type": "extension", + "name": "ext_b", + "addon": "addon_b", + "extension_group": "some_group", + "app": "localhost" + } + ], + "connections": [ + { + "extension_group": "some_group", + "extension": "ext_a", + "cmd": [ + { + "name": "cmd_1", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_b" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/ten_packages/extension/addon_a/manifest.json b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/ten_packages/extension/addon_a/manifest.json new file mode 100644 index 0000000000..05434a5648 --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/ten_packages/extension/addon_a/manifest.json @@ -0,0 +1,18 @@ +{ + "type": "extension", + "name": "addon_a", + "version": "0.1.0", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_go", + "version": "0.1.0" + } + ], + "package": { + "include": [ + "**" + ] + }, + "api": {} +} \ No newline at end of file diff --git a/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/ten_packages/extension/addon_b/manifest.json b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/ten_packages/extension/addon_b/manifest.json new file mode 100644 index 0000000000..9821e29f4d --- /dev/null +++ b/core/src/ten_manager/tests/test_data/cmd_check_single_app_node_cannot_be_localhost/ten_packages/extension/addon_b/manifest.json @@ -0,0 +1,18 @@ +{ + "type": "extension", + "name": "addon_b", + "version": "0.1.0", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_go", + "version": "0.1.0" + } + ], + "package": { + "include": [ + "**" + ] + }, + "api": {} +} \ No newline at end of file diff --git a/core/src/ten_runtime/app/graph.c b/core/src/ten_runtime/app/graph.c index fa0f2be503..1ac8521c64 100644 --- a/core/src/ten_runtime/app/graph.c +++ b/core/src/ten_runtime/app/graph.c @@ -36,7 +36,8 @@ bool ten_app_check_start_graph_cmd_json(ten_app_t *self, start_graph_cmd_json, TEN_STR_UNDERLINE_TEN, &free_json_string); const char *err_msg = NULL; - bool rc = ten_rust_check_graph_for_app(base_dir, graph_json_str, &err_msg); + bool rc = ten_rust_check_graph_for_app(base_dir, graph_json_str, + ten_app_get_uri(self), &err_msg); if (free_json_string) { TEN_FREE(graph_json_str); diff --git a/core/src/ten_rust/src/pkg_info/binding.rs b/core/src/ten_rust/src/pkg_info/binding.rs index 56a29457cb..089ed73a5d 100644 --- a/core/src/ten_rust/src/pkg_info/binding.rs +++ b/core/src/ten_rust/src/pkg_info/binding.rs @@ -10,20 +10,25 @@ use std::ffi::{c_char, CStr, CString}; pub extern "C" fn ten_rust_check_graph_for_app( app_base_dir: *const c_char, graph_json: *const c_char, + app_uri: *const c_char, out_err_msg: *mut *const c_char, ) -> bool { assert!(!app_base_dir.is_null(), "Invalid argument."); assert!(!graph_json.is_null(), "Invalid argument."); + assert!(!app_uri.is_null(), "Invalid argument."); let c_app_base_dir = unsafe { CStr::from_ptr(app_base_dir) }; let c_graph_json = unsafe { CStr::from_ptr(graph_json) }; + let c_app_uri = unsafe { CStr::from_ptr(app_uri) }; let rust_app_base_dir = c_app_base_dir.to_str().unwrap(); let rust_graph_json = c_graph_json.to_str().unwrap(); + let rust_app_uri = c_app_uri.to_str().unwrap(); let ret = crate::pkg_info::ten_rust_check_graph_for_app( rust_app_base_dir, rust_graph_json, + rust_app_uri, ); if ret.is_err() { let err_msg = ret.err().unwrap().to_string(); diff --git a/core/src/ten_rust/src/pkg_info/graph/constants.rs b/core/src/ten_rust/src/pkg_info/graph/constants.rs new file mode 100644 index 0000000000..28f2366b56 --- /dev/null +++ b/core/src/ten_rust/src/pkg_info/graph/constants.rs @@ -0,0 +1,21 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +pub const ERR_MSG_APP_LOCALHOST_DISALLOWED_SINGLE: &str = + "'localhost' is not allowed in graph definition, and the graph seems to be a single-app graph, just remove the 'app' field"; + +pub const ERR_MSG_APP_LOCALHOST_DISALLOWED_MULTI: &str = + "'localhost' is not allowed in graph definition, change the content of 'app' field to be consistent with '_ten::uri'"; + +pub const ERR_MSG_APP_DECLARATION_MISMATCH: &str = "Either all nodes should have 'app' declared, or none should, but not a mix of both."; + +pub const ERR_MSG_APP_IS_EMPTY: &str = "the 'app' field can not be empty, remove the field if the graph is a single-app graph"; + +pub const ERR_MSG_APP_SHOULD_NOT_DECLARED: &str = + "the 'app' should not be declared, as not any node has declared it"; + +pub const ERR_MSG_APP_SHOULD_DECLARED: &str = + "the 'app' can not be none, as it has been declared in nodes"; diff --git a/core/src/ten_rust/src/pkg_info/graph/mod.rs b/core/src/ten_rust/src/pkg_info/graph/mod.rs index da172ce8f8..3706673d95 100644 --- a/core/src/ten_rust/src/pkg_info/graph/mod.rs +++ b/core/src/ten_rust/src/pkg_info/graph/mod.rs @@ -5,6 +5,7 @@ // Refer to the "LICENSE" file in the root directory for more information. // mod check; +mod constants; use std::{collections::HashMap, str::FromStr}; @@ -14,6 +15,86 @@ use serde::{Deserialize, Serialize}; use super::{pkg_type::PkgType, PkgInfo}; use crate::pkg_info::localhost; +/// The state of the 'app' field declaration in all nodes in the graph. +/// +/// There might be the following cases for the 'app' field declaration: +/// +/// - Case 1: neither of the nodes has declared the 'app' field. The state will +/// be `AllNone`. +/// +/// - Case 2: all nodes have declared the 'app' field, and all of them have the +/// same value. Ex: +/// +/// { +/// "nodes": [ +/// { +/// "type": "extension", +/// "app": "http://localhost:8000", +/// "addon": "addon_1", +/// "name": "ext_1", +/// "extension_group": "some_group" +/// }, +/// { +/// "type": "extension", +/// "app": "http://localhost:8000", +/// "addon": "addon_2", +/// "name": "ext_2", +/// "extension_group": "another_group" +/// } +/// ] +/// } +/// +/// The state will be `Uniform`. +/// +/// - Case 3: all nodes have declared the 'app' field, but they have different +/// values. +/// +/// { +/// "nodes": [ +/// { +/// "type": "extension", +/// "app": "http://localhost:8000", +/// "addon": "addon_1", +/// "name": "ext_1", +/// "extension_group": "some_group" +/// }, +/// { +/// "type": "extension", +/// "app": "msgpack://localhost:8001", +/// "addon": "addon_2", +/// "name": "ext_2", +/// "extension_group": "another_group" +/// } +/// ] +/// } +/// +/// The state will be `Mixed`. +/// +/// - Case 4: some nodes have declared the 'app' field, and some have not. It's +/// illegal. +/// +/// In the view of the 'app' field declaration, there are two types of graphs: +/// +/// * Single-app graph: the state is `AllNone` or `Uniform`. +/// * Multi-app graph: the state is `Mixed`. + +#[derive(Debug, Clone, PartialEq)] +pub enum GraphNodeAppDeclaration { + NoneDeclared, + UniformDeclared, + MixedDeclared, +} + +impl GraphNodeAppDeclaration { + pub fn is_single_app_graph(&self) -> bool { + match self { + GraphNodeAppDeclaration::NoneDeclared => true, + GraphNodeAppDeclaration::UniformDeclared => true, + GraphNodeAppDeclaration::MixedDeclared => false, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Graph { // It's invalid that a graph does not contain any nodes. @@ -37,13 +118,23 @@ impl FromStr for Graph { } impl Graph { - pub fn validate_and_complete(&mut self) -> Result<()> { + fn determine_graph_node_app_declaration( + &self, + ) -> Result { let mut nodes_have_declared_app = 0; - - for (idx, node) in &mut self.nodes.iter_mut().enumerate() { - node.validate_and_complete() - .map_err(|e| anyhow::anyhow!("nodes[{}]: {}", idx, e))?; - if node.get_app_uri() != localhost() { + let mut app_uris = std::collections::HashSet::new(); + + for (idx, node) in self.nodes.iter().enumerate() { + if let Some(app_uri) = &node.app { + if app_uri.is_empty() { + return Err(anyhow::anyhow!( + "nodes[{}]: {}", + idx, + constants::ERR_MSG_APP_IS_EMPTY + )); + } + + app_uris.insert(app_uri); nodes_have_declared_app += 1; } } @@ -51,13 +142,31 @@ impl Graph { if nodes_have_declared_app != 0 && nodes_have_declared_app != self.nodes.len() { - return Err(anyhow::anyhow!("Either all nodes should have 'app' declared, or none should, but not a mix of both.")); + return Err(anyhow::anyhow!( + constants::ERR_MSG_APP_DECLARATION_MISMATCH + )); + } + + match app_uris.len() { + 0 => Ok(GraphNodeAppDeclaration::NoneDeclared), + 1 => Ok(GraphNodeAppDeclaration::UniformDeclared), + _ => Ok(GraphNodeAppDeclaration::MixedDeclared), + } + } + + pub fn validate_and_complete(&mut self) -> Result<()> { + let graph_node_app_declaration = + self.determine_graph_node_app_declaration()?; + + for (idx, node) in &mut self.nodes.iter_mut().enumerate() { + node.validate_and_complete(&graph_node_app_declaration) + .map_err(|e| anyhow::anyhow!("nodes[{}]: {}", idx, e))?; } if let Some(connections) = &mut self.connections { for (idx, connection) in connections.iter_mut().enumerate() { connection - .validate_and_complete(nodes_have_declared_app > 0) + .validate_and_complete(&graph_node_app_declaration) .map_err(|e| { anyhow::anyhow!("connections[{}].{}", idx, e) })?; @@ -120,8 +229,11 @@ pub struct GraphNode { } impl GraphNode { - fn validate_and_complete(&mut self) -> Result<()> { - // extension node must specify extension_group name. + fn validate_and_complete( + &mut self, + graph_node_app_declaration: &GraphNodeAppDeclaration, + ) -> Result<()> { + // Extension node must specify extension_group name. if self.node_type == PkgType::Extension && self.extension_group.is_none() { @@ -133,9 +245,14 @@ impl GraphNode { if let Some(app) = &self.app { if app.as_str() == localhost() { - return Err(anyhow::anyhow!( - "the app uri should be some string other than 'localhost'" - )); + let err_msg = + if graph_node_app_declaration.is_single_app_graph() { + constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_SINGLE + } else { + constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_MULTI + }; + + return Err(anyhow::anyhow!(err_msg)); } } else { self.app = Some(localhost().to_string()); @@ -172,21 +289,34 @@ pub struct GraphConnection { impl GraphConnection { fn validate_and_complete( &mut self, - is_app_declared_in_nodes: bool, + graph_node_app_declaration: &GraphNodeAppDeclaration, ) -> Result<()> { if let Some(app) = &self.app { - if !is_app_declared_in_nodes { - return Err(anyhow::anyhow!("the 'app' should not be declared, as not any node has declared it")); + if app.as_str() == localhost() { + let err_msg = + if graph_node_app_declaration.is_single_app_graph() { + constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_SINGLE + } else { + constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_MULTI + }; + + return Err(anyhow::anyhow!(err_msg)); } - if app.as_str() == localhost() { + if *graph_node_app_declaration + == GraphNodeAppDeclaration::NoneDeclared + { return Err(anyhow::anyhow!( - "the app uri should be some string other than 'localhost'" + constants::ERR_MSG_APP_SHOULD_NOT_DECLARED )); } } else { - if is_app_declared_in_nodes { - return Err(anyhow::anyhow!("the 'app' can not be none, as it has been declared in nodes.")); + if *graph_node_app_declaration + != GraphNodeAppDeclaration::NoneDeclared + { + return Err(anyhow::anyhow!( + constants::ERR_MSG_APP_SHOULD_DECLARED + )); } self.app = Some(localhost().to_string()); @@ -195,7 +325,7 @@ impl GraphConnection { if let Some(cmd) = &mut self.cmd { for (idx, cmd_flow) in cmd.iter_mut().enumerate() { cmd_flow - .validate_and_complete(is_app_declared_in_nodes) + .validate_and_complete(graph_node_app_declaration) .map_err(|e| anyhow::anyhow!("cmd[{}].{}", idx, e))?; } } @@ -203,7 +333,7 @@ impl GraphConnection { if let Some(data) = &mut self.data { for (idx, data_flow) in data.iter_mut().enumerate() { data_flow - .validate_and_complete(is_app_declared_in_nodes) + .validate_and_complete(graph_node_app_declaration) .map_err(|e| anyhow::anyhow!("data[{}].{}", idx, e))?; } } @@ -211,7 +341,7 @@ impl GraphConnection { if let Some(audio_frame) = &mut self.audio_frame { for (idx, audio_flow) in audio_frame.iter_mut().enumerate() { audio_flow - .validate_and_complete(is_app_declared_in_nodes) + .validate_and_complete(graph_node_app_declaration) .map_err(|e| { anyhow::anyhow!("audio_frame[{}].{}", idx, e) })?; @@ -221,7 +351,7 @@ impl GraphConnection { if let Some(video_frame) = &mut self.video_frame { for (idx, video_flow) in video_frame.iter_mut().enumerate() { video_flow - .validate_and_complete(is_app_declared_in_nodes) + .validate_and_complete(graph_node_app_declaration) .map_err(|e| { anyhow::anyhow!("video_frame[{}].{}", idx, e) })?; @@ -245,10 +375,10 @@ pub struct GraphMessageFlow { impl GraphMessageFlow { fn validate_and_complete( &mut self, - is_app_declared_in_nodes: bool, + graph_node_app_declaration: &GraphNodeAppDeclaration, ) -> Result<()> { for (idx, dest) in &mut self.dest.iter_mut().enumerate() { - dest.validate_and_complete(is_app_declared_in_nodes) + dest.validate_and_complete(graph_node_app_declaration) .map_err(|e| anyhow::anyhow!("dest[{}]: {}", idx, e))?; } @@ -268,21 +398,34 @@ pub struct GraphDestination { impl GraphDestination { fn validate_and_complete( &mut self, - is_app_declared_in_nodes: bool, + graph_node_app_declaration: &GraphNodeAppDeclaration, ) -> Result<()> { if let Some(app) = &self.app { - if !is_app_declared_in_nodes { - return Err(anyhow::anyhow!("the 'app' should not be declared, as not any node has declared it")); + if app.as_str() == localhost() { + let err_msg = + if graph_node_app_declaration.is_single_app_graph() { + constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_SINGLE + } else { + constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_MULTI + }; + + return Err(anyhow::anyhow!(err_msg)); } - if app.as_str() == localhost() { + if *graph_node_app_declaration + == GraphNodeAppDeclaration::NoneDeclared + { return Err(anyhow::anyhow!( - "the app uri should be some string other than 'localhost'" + constants::ERR_MSG_APP_SHOULD_NOT_DECLARED )); } } else { - if is_app_declared_in_nodes { - return Err(anyhow::anyhow!("the 'app' can not be none, as it has been declared in nodes.")); + if *graph_node_app_declaration + != GraphNodeAppDeclaration::NoneDeclared + { + return Err(anyhow::anyhow!( + constants::ERR_MSG_APP_SHOULD_DECLARED + )); } self.app = Some(localhost().to_string()); @@ -383,6 +526,55 @@ mod tests { println!("Error: {:?}", result.err().unwrap()); } + #[test] + fn test_predefined_graph_node_app_localhost() { + let property_str = include_str!( + "test_data_embed/predefined_graph_connection_app_localhost.json" + ); + let property = Property::from_str(property_str); + + // 'localhost' is not allowed in graph definition. + assert!(property.is_err()); + println!("Error: {:?}", property); + + let msg = property.err().unwrap().to_string(); + assert!( + msg.contains(constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_SINGLE) + ); + } + + #[test] + fn test_start_graph_cmd_single_app_node_app_localhost() { + let graph_str = include_str!( + "test_data_embed/start_graph_cmd_single_app_node_app_localhost.json" + ); + let graph = Graph::from_str(graph_str); + + // 'localhost' is not allowed in graph definition. + assert!(graph.is_err()); + println!("Error: {:?}", graph); + + let msg = graph.err().unwrap().to_string(); + assert!( + msg.contains(constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_SINGLE) + ); + } + + #[test] + fn test_start_graph_cmd_multi_apps_node_app_localhost() { + let graph_str = include_str!( + "test_data_embed/start_graph_cmd_multi_apps_node_app_localhost.json" + ); + let graph = Graph::from_str(graph_str); + + // 'localhost' is not allowed in graph definition. + assert!(graph.is_err()); + println!("Error: {:?}", graph); + + let msg = graph.err().unwrap().to_string(); + assert!(msg.contains(constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_MULTI)); + } + #[test] fn test_predefined_graph_connection_app_localhost() { let property_str = include_str!( @@ -390,9 +582,14 @@ mod tests { ); let property = Property::from_str(property_str); - // App uri should be some string other than 'localhost'. + // 'localhost' is not allowed in graph definition. assert!(property.is_err()); - println!("Error: {:?}", property.err().unwrap()); + println!("Error: {:?}", property); + + let msg = property.err().unwrap().to_string(); + assert!( + msg.contains(constants::ERR_MSG_APP_LOCALHOST_DISALLOWED_SINGLE) + ); } #[test] @@ -523,4 +720,19 @@ mod tests { let result = graph.check_message_names(); assert!(result.is_ok()); } + + #[test] + fn test_graph_app_can_not_be_empty_string() { + let graph_str = include_str!( + "test_data_embed/graph_app_can_not_be_empty_string.json" + ); + let graph = Graph::from_str(graph_str); + + // The 'app' can not be empty string. + assert!(graph.is_err()); + println!("Error: {:?}", graph); + + let msg = graph.err().unwrap().to_string(); + assert!(msg.contains(constants::ERR_MSG_APP_IS_EMPTY)); + } } diff --git a/core/src/ten_rust/src/pkg_info/graph/test_data_embed/graph_app_can_not_be_empty_string.json b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/graph_app_can_not_be_empty_string.json new file mode 100644 index 0000000000..2e9dea3f8f --- /dev/null +++ b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/graph_app_can_not_be_empty_string.json @@ -0,0 +1,19 @@ +{ + "type": "start_graph", + "seq_id": "55", + "nodes": [ + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_2__test_extension", + "extension_group": "test_extension_group", + "app": "" + }, + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_1__test_extension", + "extension_group": "test_extension_group" + } + ] +} \ No newline at end of file diff --git a/core/src/ten_rust/src/pkg_info/graph/test_data_embed/predefined_graph_node_app_localhost.json b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/predefined_graph_node_app_localhost.json new file mode 100644 index 0000000000..1989aa790a --- /dev/null +++ b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/predefined_graph_node_app_localhost.json @@ -0,0 +1,36 @@ +{ + "_ten": { + "predefined_graphs": [ + { + "name": "default", + "auto_start": false, + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "default_extension_go", + "extension_group": "some_group", + "app": "localhost" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "producer", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "some_extension" + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/core/src/ten_rust/src/pkg_info/graph/test_data_embed/start_graph_cmd_multi_apps_node_app_localhost.json b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/start_graph_cmd_multi_apps_node_app_localhost.json new file mode 100644 index 0000000000..c4a468426b --- /dev/null +++ b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/start_graph_cmd_multi_apps_node_app_localhost.json @@ -0,0 +1,20 @@ +{ + "type": "start_graph", + "seq_id": "55", + "nodes": [ + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_2__test_extension", + "extension_group": "test_extension_group", + "app": "msgpack://127.0.0.1:8001/" + }, + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_1__test_extension", + "extension_group": "test_extension_group", + "app": "localhost" + } + ] +} \ No newline at end of file diff --git a/core/src/ten_rust/src/pkg_info/graph/test_data_embed/start_graph_cmd_single_app_node_app_localhost.json b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/start_graph_cmd_single_app_node_app_localhost.json new file mode 100644 index 0000000000..aea3906de3 --- /dev/null +++ b/core/src/ten_rust/src/pkg_info/graph/test_data_embed/start_graph_cmd_single_app_node_app_localhost.json @@ -0,0 +1,20 @@ +{ + "type": "start_graph", + "seq_id": "55", + "nodes": [ + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_2__test_extension", + "extension_group": "test_extension_group", + "app": "localhost" + }, + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_1__test_extension", + "extension_group": "test_extension_group", + "app": "localhost" + } + ] +} \ No newline at end of file diff --git a/core/src/ten_rust/src/pkg_info/mod.rs b/core/src/ten_rust/src/pkg_info/mod.rs index 45381dd987..a2ee35cf97 100644 --- a/core/src/ten_rust/src/pkg_info/mod.rs +++ b/core/src/ten_rust/src/pkg_info/mod.rs @@ -29,7 +29,7 @@ use std::{ str::FromStr, }; -use anyhow::{Context, Result}; +use anyhow::Result; use graph::Graph; use semver::Version; @@ -404,9 +404,19 @@ pub fn find_to_be_replaced_local_pkgs<'a>( result } +/// Check the graph for current app. +/// +/// # Arguments +/// * `app_base_dir` - The absolute path of the app base directory, required. +/// * `graph_json` - The graph definition in JSON format, required. +/// * `app_uri` - The `_ten::uri` of the app, required. We do not read this +/// content from property.json of app, because the property.json might not +/// exist. Ex: the property of app might be customized using +/// `init_property_from_json` in `on_configure` method. pub fn ten_rust_check_graph_for_app( app_base_dir: &str, graph_json: &str, + app_uri: &str, ) -> Result<()> { let app_path = Path::new(app_base_dir); if !app_path.exists() { @@ -416,14 +426,10 @@ pub fn ten_rust_check_graph_for_app( )); } - let property = parse_property_in_folder(app_path)?; - let mut pkgs_of_app: HashMap> = HashMap::new(); let pkgs_info = get_all_existed_pkgs_info_of_app(app_path)?; - pkgs_of_app.insert(property.get_app_uri(), pkgs_info); - - let graph = Graph::from_str(graph_json) - .with_context(|| "The graph json string is invalid.")?; + pkgs_of_app.insert(app_uri.to_string(), pkgs_info); + let graph = Graph::from_str(graph_json)?; graph.check_for_single_app(&pkgs_of_app) } diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 471ffea820..7673ffac22 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -57,6 +57,7 @@ * [Overview](ten_framework/ten_manager/overview.md) * [Dev-Server](ten_framework/ten_manager/dev_server_cn.md) +* [Check-Graph](ten_framework/ten_manager/check_graph.md) ## Tutorials diff --git a/docs/ten_framework/graph.md b/docs/ten_framework/graph.md index 9d10dbd60e..342d13c972 100644 --- a/docs/ten_framework/graph.md +++ b/docs/ten_framework/graph.md @@ -366,3 +366,213 @@ The following is a complete definition of the `start_graph` command: } } ``` + +## Specification for Graph Definition + +- **Requirement for `nodes` Field**: + The `nodes` array is mandatory in a graph definition. Conversely, the `connections` array is optional but encouraged for defining inter-node communication. + +- **Validation of Node `app` Field**: + The `app` field must never be set to `localhost` under any circumstances. In a single-app graph, the `app` URI should not be specified. In a multi-app graph, the value of the `app` field must match the `_ten::uri` value defined in each app's `property.json`. + +- **Node Uniqueness and Identification**: + Each node in the `nodes` array represents a specific extension instance within a group of an app, created by a specified addon. Therefore, each extension instance should be uniquely represented by a single node. A node must be uniquely identified by the combination of `app`, `extension_group`, and `name`. Multiple entries for the same extension instance are not allowed. The following example is invalid because it defines multiple nodes for the same extension instance: + + ```json + { + "nodes": [ + { + "type": "extension", + "name": "some_ext", + "addon": "addon_1", + "extension_group": "test" + }, + { + "type": "extension", + "name": "some_ext", + "addon": "addon_2", + "extension_group": "test" + } + ] + } + ``` + +- **Consistency of Extension Instance Definition in Connections**: + All extension instances referenced in the `connections` field, whether as a source or destination, must be explicitly defined in the `nodes` field. Any instance not defined in the `nodes` array will cause validation errors. + + For example, the following is invalid because the extension instance `ext_2` is used in the `connections` field but is not defined in the `nodes` field: + + ```json + { + "nodes": [ + { + "type": "extension", + "name": "ext_1", + "addon": "addon_1", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension_group": "some_group", + "extension": "ext_1", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_2" + } + ] + } + ] + } + ] + } + ``` + +- **Consolidation of Connection Definitions**: + Within the `connections` array, all messages related to the same source extension instance must be grouped within a single section. Splitting the information across multiple sections for the same source extension instance leads to inconsistencies and errors. + + For example, the following is incorrect because the messages from `ext_1` are divided into separate sections: + + ```json + { + "connections": [ + { + "extension_group": "some_group", + "extension": "ext_1", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_2" + } + ] + } + ] + }, + { + "extension_group": "some_group", + "extension": "ext_1", + "data": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_2" + } + ] + } + ] + } + ] + } + ``` + + The correct approach is to consolidate all messages for the same source extension instance into one section: + + ```json + { + "connections": [ + { + "extension_group": "some_group", + "extension": "ext_1", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_2" + } + ] + } + ], + "data": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_2" + } + ] + } + ] + } + ] + } + ``` + +- **Consolidation of Destinations for Unique Messages**: + For each message within a specific type (e.g., `cmd` or `data`), the destination extension instances must be grouped under a single entry for that message. Repeating the same message name with separate destinations leads to inconsistency and validation errors. + + For example, the following is incorrect due to separate entries for the message named `hello`: + + ```json + { + "connections": [ + { + "extension_group": "some_group", + "extension": "ext_1", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_2" + } + ] + }, + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_3" + } + ] + } + ] + } + ] + } + ``` + + The correct approach is to consolidate all destinations for the same message under a single entry: + + ```json + { + "connections": [ + { + "extension_group": "some_group", + "extension": "ext_1", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "ext_2" + }, + { + "extension_group": "some_group", + "extension": "ext_3" + } + ] + } + ] + } + ] + } + ``` + + However, messages with the same name can exist across different types, such as `cmd` and `data`, without causing conflicts. + +For further examples, refer to the `check graph` command documentation within the TEN framework's `tman`. diff --git a/docs/ten_framework/ten_manager/check_graph.md b/docs/ten_framework/ten_manager/check_graph.md new file mode 100644 index 0000000000..59e5cd0ea5 --- /dev/null +++ b/docs/ten_framework/ten_manager/check_graph.md @@ -0,0 +1,856 @@ +# TEN Manager - Check Graph + +`tman` provides the `check graph` command to validate predefined graphs or a `start_graph` command for correctness. To see the usage details, use the following command: + +```shell +$ tman check graph -h +Check whether the graph content of the predefined graph or start_graph command is correct. For more detailed usage, run 'graph -h' + +Usage: tman check graph [OPTIONS] --app + +Options: + --app + The absolute path of the app defined in the graph. By default, the predefined graph will be read from the first one in the list. + --predefined-graph-name + Specify a predefined graph name to check, otherwise, all predefined graphs will be checked. + --graph + Specify the JSON string of a 'start_graph' command to be checked. If not specified, the predefined graph in the first app will be checked. + -h, --help + Print help +``` + +The `check graph` command is designed to handle graphs that may span multiple apps, so it does not require being run from the root directory of a specific app. Instead, it can be executed from any directory, with the `--app` parameter used to specify the folders of the apps involved in the graph. + +## Example Usages + +- **Check all predefined graphs in `property.json`**: + + ```shell + tman check graph --app /home/TEN-Agent/agents + ``` + +- **Check a specific predefined graph**: + + ```shell + tman check graph --predefined-graph-name va.openai.azure --app /home/TEN-Agent/agents + ``` + +- **Check a `start_graph` command**: + + ```shell + tman check graph --graph '{ + "type": "start_graph", + "seq_id": "55", + "nodes": [ + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_2__test_extension", + "extension_group": "test_extension_group", + "app": "msgpack://127.0.0.1:8001/" + }, + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_1__test_extension", + "extension_group": "test_extension_group", + "app": "msgpack://127.0.0.1:8001/" + } + ] + }' --app /home/TEN-Agent/agents + ``` + +## Prerequisites + +- **Predefined Graph Definition Requirement**: If a predefined graph name is specified, the definition will be extracted from the `property.json` file of the app specified by the first `--app` parameter. + +- **Package Installation Requirement**: Before running the `check graph` command, all extensions that the app depends on must be installed using the `tman install` command. This is necessary because the validation process requires information about each extension in the graph, such as APIs defined in their `manifest.json` files. + +- **Unique App URI Requirement**: In a multi-app graph, each app's `property.json` must define a unique `_ten::uri`. Additionally, the `uri` value cannot be set to `"localhost"`. + +## Validation Rules + +### 1. Presence of Nodes + +The `nodes` array is required in any graph definition. If absent, an error will be thrown. + +- **Example (No nodes defined)**: + + ```shell + tman check graph --graph '{ + "type": "start_graph", + "seq_id": "55", + "nodes": [] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + No extension node is defined in graph. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +### 2. Uniqueness of Nodes + +Each node in the `nodes` array represents a specific extension instance within a group of an app, created by a specified addon. Therefore, each extension instance should be uniquely represented by a single node. A node must be uniquely identified by the combination of `app`, `extension_group`, and `name`. Multiple entries for the same extension instance are not allowed. + +- **Example (Duplicate nodes)**: + + ```shell + tman check graph --graph '{ + "type": "start_graph", + "seq_id": "55", + "nodes": [ + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_2__test_extension", + "extension_group": "test_extension_group", + "app": "msgpack://127.0.0.1:8001/" + }, + { + "type": "extension", + "name": "test_extension", + "addon": "basic_hello_world_1__test_extension", + "extension_group": "test_extension_group", + "app": "msgpack://127.0.0.1:8001/" + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + Duplicated extension was found in nodes[1], addon: basic_hello_world_1__test_extension, name: test_extension. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +### 3. Extensions used in connections should be defined in nodes + +All extension instances referenced in the `connections` field, whether as a source or destination, must be explicitly defined in the `nodes` field. Any instance not defined in the `nodes` array will cause validation errors. + +- **Example (Source extension not defined)**: + + Imagine that the content of `property.json` of a TEN app is as follows. + + ```json + { + "_ten": { + "predefined_graphs": [ + { + "name": "default", + "auto_start": false, + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "default_extension_go", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "producer", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "some_extension" + } + ] + } + ] + } + ] + } + ] + } + } + ``` + + ```shell + tman check graph --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + The extension declared in connections[0] is not defined in nodes, extension_group: producer, extension: some_extension. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +- **Example (Destination extension not defined)**: + + Imagine that the content of `property.json` of a TEN app is as follows. + + ```json + { + "_ten": { + "predefined_graphs": [ + { + "name": "default", + "auto_start": false, + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "default_extension_go", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "some_extension_1", + "addon": "default_extension_go", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "some_extension_1" + } + ] + }, + { + "name": "world", + "dest": [ + { + "extension_group": "some_group", + "extension": "some_extension_1" + }, + { + "extension_group": "some_group", + "extension": "consumer" + } + ] + } + ] + } + ] + } + ] + } + } + ``` + + ```shell + tman check graph --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + The extension declared in connections[0].cmd[1] is not defined in nodes extension_group: some_group, extension: consumer. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +### 4. The addons declared in the `nodes` must be installed in the app + +- **Example (The `_ten::uri` in property.json is not equal to the `app` field in nodes)**: + + Imagine that the content of property.json of a TEN app is as follows. + + ```json + { + "_ten": { + "predefined_graphs": [ + { + "name": "default", + "auto_start": false, + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "default_extension_go", + "extension_group": "some_group" + } + ] + } + ], + "uri": "http://localhost:8001" + } + } + ``` + + ```shell + tman check graph --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + The following packages are declared in nodes but not installed: [("localhost", Extension, "default_extension_go")]. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + + The problem is all packages in the app will be stored in a map which key is the `uri` of the app, and each node in the graph is retrieved by the `app` field (which is `localhost` by default). The `app` in node (i.e., localhost) is mismatch with the `uri` of app (i.e., ). + +- **Example (the ten_packages does not exist as the `tman install` has not executed)**: + + Imagine that the content of property.json of a TEN app is as follows. + + ```json + { + "_ten": { + "predefined_graphs": [ + { + "name": "default", + "auto_start": false, + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "default_extension_go", + "extension_group": "some_group" + } + ] + } + ] + } + } + ``` + + And the `ten_packages` directory does **\_NOT** exist. + + ```shell + tman check graph --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + The following packages are declared in nodes but not installed: [("localhost", Extension, "default_extension_go")]. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +### 5. In connections, messages sent from one extension should be defined in the same section + +- **Example**: + + Imagine that all the packages have been installed. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "default_extension_go", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "default_extension_go", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext" + } + ] + } + ] + }, + { + "extension": "some_extension", + "extension_group": "some_group", + "cmd": [ + { + "name": "hello_2", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext" + } + ] + } + ] + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + extension 'some_extension' is defined in connection[0] and connection[1], merge them into one section. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +### 6. In connections, the messages sent out from one extension should have a unique name in each type + +- **Example**: + + Imagine that all the packages have been installed. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "addon_a", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "addon_b", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "cmd": [ + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext" + } + ] + }, + { + "name": "hello", + "dest": [ + { + "extension_group": "some_group", + "extension": "some_extension" + } + ] + } + ] + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + - connection[0]: + - Merge the following cmd into one section: + 'hello' is defined in flow[0] and flow[1]. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +### 7. The messages declared in the connections should be compatible + +The message declared in each message flow in the connections will be checked if the schema is compatible, according to the schema definition in the manifest.json of extensions. The rules of message compatible are as follows. + +- If the message schema is not found neither in the source extension nor the target extension, the message is compatible. +- If the message schema is only found in one of the extensions, the message is incompatible. +- The message is compatible only if the following conditions are met. + + - The property type is compatible. + - If the property is an object, the fields both in the source and target schema must have a compatible type. + - If the `required` keyword is defined in the target schema, there must be a `required` keyword in the source schema, which is the superset of the `required` in the target. + +- **Example**: + + Imagine that all the packages have been installed. + + The content of manifest.json in `addon_a` is as follows. + + ```json + { + "type": "extension", + "name": "addon_a", + "version": "0.1.0", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_go", + "version": "0.1.0" + } + ], + "package": { + "include": ["**"] + }, + "api": { + "cmd_out": [ + { + "name": "cmd_1", + "property": { + "foo": { + "type": "string" + } + } + } + ] + } + } + ``` + + And, the content of manifest.json in `addon_b` is as follows. + + ```json + { + "type": "extension", + "name": "addon_b", + "version": "0.1.0", + "dependencies": [ + { + "type": "system", + "name": "ten_runtime_go", + "version": "0.1.0" + } + ], + "package": { + "include": ["**"] + }, + "api": { + "cmd_in": [ + { + "name": "cmd_1", + "property": { + "foo": { + "type": "int8" + } + } + } + ] + } + } + ``` + + Checking graph with the following command. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "addon_a", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "addon_b", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "cmd": [ + { + "name": "cmd_1", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext" + } + ] + } + ] + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + Checking graph[0]... ❌. Details: + - connections[0]: + - cmd[0]: Schema incompatible to [extension_group: some_group, extension: another_ext], properties are incompatible: + property [foo], Type is incompatible, source is [string], but target is [int8]. + + All is done. + 💔 Error: 1/1 graphs failed. + ``` + +### 8. The `app` in node must be unambiguous + +The `app` field in each node must met the following rules. + +- The `app` field must be equal to the `_ten::uri` of the corresponding TEN app. +- Either all nodes should have `app` declared, or none should. +- The `app` field can not be `localhost`. +- The `app` field can not be an empty string. + +- **Example (some of the nodes specified the `app` field)**: + + Checking graph with the following command. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "addon_a", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "addon_b", + "extension_group": "some_group", + "app": "http://localhost:8000" + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + 💔 Error: The graph json string is invalid + + Caused by: + Either all nodes should have 'app' declared, or none should, but not a mix of both. + ``` + +- **Example (No `app` specified in all nodes, but some source extension specified `app` in the connections)**: + + Checking graph with the following command. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "addon_a", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "addon_b", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "app": "http://localhost:8000", + "cmd": [ + { + "name": "cmd_1", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext" + } + ] + } + ] + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + 💔 Error: The graph json string is invalid + + Caused by: + connections[0].the 'app' should not be declared, as not any node has declared it + ``` + +- **Example (No `app` specified in all nodes, but some target extension specified `app` in the connections)**: + + Checking graph with the following command. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "addon_a", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "addon_b", + "extension_group": "some_group" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "cmd": [ + { + "name": "cmd_1", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext", + "app": "http://localhost:8000" + } + ] + } + ] + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + 💔 Error: The graph json string is invalid + + Caused by: + connections[0].cmd[0].dest[0]: the 'app' should not be declared, as not any node has declared it + ``` + +- **Example (The `app` field in nodes is not equal to the `_ten::uri` of app)**: + + Same as [Rule 4](#id-4.-the-addons-declared-in-the-nodes-must-be-installed-in-the-app). + +- **Example (The `app` field is `localhost` in a single-app graph)**: + + Checking graph with the following command. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "addon_a", + "extension_group": "some_group" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "addon_b", + "extension_group": "some_group", + "app": "localhost" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "cmd": [ + { + "name": "cmd_1", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext" + } + ] + } + ] + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + 💔 Error: Failed to parse graph string, nodes[1]: 'localhost' is not allowed in graph definition, and the graph seems to be a single-app graph, just remove the 'app' field + ``` + +- **Example (The `app` field is `localhost` in a multi-app graph)**: + + Checking graph with the following command. + + ```shell + tman check graph --graph '{ + "nodes": [ + { + "type": "extension", + "name": "some_extension", + "addon": "addon_a", + "extension_group": "some_group", + "app": "http://localhost:8000" + }, + { + "type": "extension", + "name": "another_ext", + "addon": "addon_b", + "extension_group": "some_group", + "app": "localhost" + } + ], + "connections": [ + { + "extension": "some_extension", + "extension_group": "some_group", + "app": "http://localhost:8000", + "cmd": [ + { + "name": "cmd_1", + "dest": [ + { + "extension_group": "some_group", + "extension": "another_ext" + } + ] + } + ] + } + ] + }' --app /home/TEN-Agent/agents + ``` + + **Output**: + + ```text + 💔 Error: Failed to parse graph string, nodes[1]: 'localhost' is not allowed in graph definition, change the content of 'app' field to be consistent with '_ten::uri' + ```