Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ lazy_static = "1.4"
# Actor framework for ReAct agents
ractor = "0"

# TOML deserialization (also used transitively by config)
toml = "0.8"

# Configuration file support (multi-format)
config = { version = "0.14", features = [
"toml",
Expand Down
1 change: 1 addition & 0 deletions crates/mofa-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mofa-kernel = { path = "../mofa-kernel", version = "0.1", features = [
] }
mofa-runtime = { path = "../mofa-runtime", version = "0.1" }
mofa-foundation = { path = "../mofa-foundation", version = "0.1" }
mofa-testing = { path = "../../tests", version = "0.1" }
config.workspace = true
tokio = { workspace = true }
thiserror = { workspace = true }
Expand Down
40 changes: 40 additions & 0 deletions crates/mofa-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ pub enum Commands {
dora: bool,
},

/// Run a testing DSL case file
TestDsl {
/// TOML DSL file to execute
file: PathBuf,

/// Optional report file path
#[arg(long)]
report_out: Option<PathBuf>,

/// Report file format
#[arg(long, value_enum, default_value_t = TestDslReportFormat::Json)]
report_format: TestDslReportFormat,
},

/// Run a dora dataflow
#[cfg(feature = "dora")]
Dataflow {
Expand Down Expand Up @@ -219,6 +233,12 @@ pub enum DatabaseType {
Sqlite,
}

#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
pub enum TestDslReportFormat {
Json,
Text,
}

impl std::fmt::Display for DatabaseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expand Down Expand Up @@ -725,6 +745,26 @@ mod tests {
assert!(parsed.is_ok(), "doctor ci strict json should parse");
}

#[test]
fn test_test_dsl_parses() {
let parsed = Cli::try_parse_from(["mofa", "test-dsl", "tests/examples/simple_agent.toml"]);
assert!(parsed.is_ok(), "test-dsl command should parse");
}

#[test]
fn test_test_dsl_report_flags_parse() {
let parsed = Cli::try_parse_from([
"mofa",
"test-dsl",
"tests/examples/simple_agent.toml",
"--report-out",
"/tmp/report.json",
"--report-format",
"json",
]);
assert!(parsed.is_ok(), "test-dsl report flags should parse");
}

#[test]
fn test_rag_index_parses() {
let parsed = Cli::try_parse_from([
Expand Down
1 change: 1 addition & 0 deletions crates/mofa-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ pub mod new;
pub mod plugin;
pub mod rag;
pub mod run;
pub mod test_dsl;
pub mod session;
pub mod tool;
124 changes: 124 additions & 0 deletions crates/mofa-cli/src/commands/test_dsl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! `mofa test-dsl` command implementation

use crate::CliError;
use crate::cli::TestDslReportFormat;
use crate::output::OutputFormat;
use mofa_testing::{
DslError, JsonFormatter, ReportFormatter, TestCaseResult, TestReport, TestStatus,
TextFormatter, run_test_case, TestCaseDsl,
};
use serde::Serialize;
use serde_json::json;
use std::path::Path;

#[derive(Debug, Serialize)]
struct TestDslSummary {
name: String,
success: bool,
output_text: Option<String>,
duration_ms: u128,
tool_calls: Vec<String>,
workspace_root: String,
}

/// Execute one TOML DSL test case through the testing runner.
pub async fn run(
path: &Path,
format: OutputFormat,
report_out: Option<&Path>,
report_format: TestDslReportFormat,
) -> Result<(), CliError> {
let case = TestCaseDsl::from_toml_file(path).map_err(map_dsl_error)?;
let result = run_test_case(&case).await.map_err(map_dsl_error)?;
let report = build_report(&case.name, &result);

if let Some(report_out) = report_out {
write_report(report_out, report_format, &report)?;
}

let summary = TestDslSummary {
name: case.name,
success: result.is_success(),
output_text: result.output_text(),
duration_ms: result.duration.as_millis(),
tool_calls: result
.metadata
.tool_calls
.iter()
.map(|record| record.tool_name.clone())
.collect(),
workspace_root: result.metadata.workspace_root.display().to_string(),
};

match format {
OutputFormat::Json => {
let output = json!({
"success": true,
"case": summary,
});
println!("{}", serde_json::to_string_pretty(&output)?);
}
_ => {
println!("case: {}", summary.name);
println!("status: {}", if summary.success { "passed" } else { "failed" });
if let Some(output_text) = &summary.output_text {
println!("output: {}", output_text);
}
if !summary.tool_calls.is_empty() {
println!("tool_calls: {}", summary.tool_calls.join(", "));
}
println!("duration_ms: {}", summary.duration_ms);
}
}

Ok(())
}

fn build_report(case_name: &str, result: &mofa_testing::AgentRunResult) -> TestReport {
let status = if result.is_success() {
TestStatus::Passed
} else {
TestStatus::Failed
};
let error = result.error.as_ref().map(ToString::to_string);
let metadata = vec![
(
"execution_id".to_string(),
result.metadata.execution_id.clone(),
),
(
"workspace_root".to_string(),
result.metadata.workspace_root.display().to_string(),
),
(
"tool_calls".to_string(),
result.metadata.tool_calls.len().to_string(),
),
];

TestReport {
suite_name: "dsl".to_string(),
results: vec![TestCaseResult {
name: case_name.to_string(),
status,
duration: result.duration,
error,
metadata,
}],
total_duration: result.duration,
timestamp: result.metadata.started_at.timestamp_millis() as u64,
}
}

fn write_report(path: &Path, format: TestDslReportFormat, report: &TestReport) -> Result<(), CliError> {
let body = match format {
TestDslReportFormat::Json => JsonFormatter.format(report),
TestDslReportFormat::Text => TextFormatter.format(report),
};
std::fs::write(path, body)?;
Ok(())
}

fn map_dsl_error(error: DslError) -> CliError {
CliError::Other(format!("DSL test failed: {error}"))
}
12 changes: 12 additions & 0 deletions crates/mofa-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ fn main() {

async fn run_command(cli: Cli) -> CliResult<()> {
use cli::Commands;
let output_format = cli.output_format.unwrap_or_default();

// Initialize context for commands that need backend services
let needs_context = matches!(
Expand Down Expand Up @@ -121,6 +122,17 @@ async fn run_command(cli: Cli) -> CliResult<()> {
commands::run::run(&config, dora)?;
}

Some(Commands::TestDsl {
file,
report_out,
report_format,
}) => {
commands::test_dsl::run(&file, output_format, report_out.as_deref(), report_format)
.await
.into_report()
.attach_with(|| format!("running DSL test case from {}", file.display()))?;
}

#[cfg(feature = "dora")]
Some(Commands::Dataflow { file, uv }) => {
commands::run::run_dataflow(&file, uv)?;
Expand Down
94 changes: 94 additions & 0 deletions crates/mofa-cli/tests/test_dsl_integration_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Integration tests for `mofa test-dsl`.

use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::tempdir;

#[test]
fn test_dsl_command_runs_example_case() {
let case_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/examples/simple_agent.toml"
);

Command::cargo_bin("mofa")
.expect("mofa bin")
.args(["test-dsl", case_path])
.assert()
.success()
.stdout(predicate::str::contains("status: passed"))
.stdout(predicate::str::contains("output: hello from DSL"));
}

#[test]
fn test_dsl_command_emits_json() {
let case_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/examples/tool_agent.toml"
);

Command::cargo_bin("mofa")
.expect("mofa bin")
.args(["--output-format", "json", "test-dsl", case_path])
.assert()
.success()
.stdout(predicate::str::contains("\"success\": true"))
.stdout(predicate::str::contains("\"tool_calls\""))
.stdout(predicate::str::contains("\"echo_tool\""));
}

#[test]
fn test_dsl_command_writes_json_report_file() {
let case_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/examples/simple_agent.toml"
);
let temp = tempdir().expect("temp dir");
let report_path = temp.path().join("dsl-report.json");

Command::cargo_bin("mofa")
.expect("mofa bin")
.args([
"test-dsl",
case_path,
"--report-out",
report_path.to_str().expect("utf8 report path"),
"--report-format",
"json",
])
.assert()
.success();

let report = std::fs::read_to_string(&report_path).expect("report file exists");
assert!(report.contains("\"suite\": \"dsl\""));
assert!(report.contains("\"name\": \"simple_agent_run\""));
assert!(report.contains("\"status\": \"passed\""));
}

#[test]
fn test_dsl_command_writes_text_report_file() {
let case_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/examples/tool_agent.toml"
);
let temp = tempdir().expect("temp dir");
let report_path = temp.path().join("dsl-report.txt");

Command::cargo_bin("mofa")
.expect("mofa bin")
.args([
"test-dsl",
case_path,
"--report-out",
report_path.to_str().expect("utf8 report path"),
"--report-format",
"text",
])
.assert()
.success();

let report = std::fs::read_to_string(&report_path).expect("report file exists");
assert!(report.contains("=== dsl ==="));
assert!(report.contains("tool_agent_run"));
assert!(report.contains("[+]"));
}
11 changes: 11 additions & 0 deletions crates/mofa-foundation/src/agent/context/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ impl PromptContext {
self
}

/// Replace the agent identity.
pub fn set_identity(&mut self, identity: AgentIdentity) {
self.agent_name = identity.name.clone();
self.identity = identity;
}

/// Replace the bootstrap file list.
pub fn set_bootstrap_files(&mut self, files: Vec<String>) {
self.bootstrap_files = files;
}

/// Set skills that should always be loaded
pub fn with_always_load(mut self, skills: Vec<String>) -> Self {
self.always_load = skills;
Expand Down
18 changes: 16 additions & 2 deletions crates/mofa-foundation/src/agent/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,15 @@ impl AgentExecutor {
&self.config
}

/// Update the prompt context (system prompt builder).
pub async fn update_prompt_context<F>(&self, updater: F)
where
F: FnOnce(&mut PromptContext),
{
let mut ctx = self.context.write().await;
updater(&mut ctx);
}

/// Get mutable reference to base agent
pub fn base_mut(&mut self) -> &mut BaseAgent {
&mut self.base
Expand Down Expand Up @@ -586,7 +595,9 @@ impl MoFAAgent for AgentExecutor {
self.base.initialize(ctx).await?;

// Additional executor-specific initialization
self.base.transition_to(AgentState::Ready)?;
if self.base.state() != AgentState::Ready {
self.base.transition_to(AgentState::Ready)?;
}

Ok(())
}
Expand Down Expand Up @@ -643,7 +654,10 @@ mod tests {
"mock"
}

async fn chat(&self, _request: ChatCompletionRequest) -> AgentResult<ChatCompletionResponse> {
async fn chat(
&self,
_request: ChatCompletionRequest,
) -> AgentResult<ChatCompletionResponse> {
Ok(ChatCompletionResponse {
content: Some("ok".to_string()),
tool_calls: Some(Vec::<ToolCall>::new()),
Expand Down
Loading
Loading