Skip to content
Draft
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
273 changes: 169 additions & 104 deletions src/tools/builder/core/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,44 @@
//! result-shape invariants without invoking the full LLM-driven build loop.

use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
use std::path::Path;

mod assertions {
use super::*;
use pretty_assertions::assert_eq;

#[track_caller]
pub(super) fn assert_build_requirement_roundtrip(req: &BuildRequirement) {
let json = serde_json::to_string(req).expect("serialize BuildRequirement");
let deserialized: BuildRequirement =
serde_json::from_str(&json).expect("deserialize BuildRequirement");
assert_eq!(
(
&deserialized.name,
&deserialized.description,
&deserialized.software_type,
&deserialized.language,
&deserialized.input_spec,
&deserialized.output_spec,
&deserialized.dependencies,
&deserialized.capabilities,
),
(
&req.name,
&req.description,
&req.software_type,
&req.language,
&req.input_spec,
&req.output_spec,
&req.dependencies,
&req.capabilities,
)
);
}

#[track_caller]
pub(super) fn assert_build_success(res: &BuildResult) {
assert!(res.success, "expected build to succeed");
assert!(
Expand All @@ -18,6 +51,7 @@ mod assertions {
);
}

#[track_caller]
pub(super) fn assert_build_failure_contains(res: &BuildResult, needle: &str) {
assert!(!res.success, "expected build to fail");
assert!(
Expand All @@ -30,6 +64,83 @@ mod assertions {
);
}

#[track_caller]
pub(super) fn assert_build_result_success(res: &BuildResult) {
assert_build_success(res);
assert_eq!(
(
res.iterations,
res.tests_passed,
res.tests_failed,
res.registered
),
(3, 5, 0, true)
);
}

#[track_caller]
pub(super) fn assert_build_result_failure(
res: &BuildResult,
expected_error: &str,
expected_warnings: usize,
tests_passed: u32,
tests_failed: u32,
) {
assert_build_failure_contains(res, expected_error);
assert_eq!(
(
res.validation_warnings.len(),
res.tests_passed,
res.tests_failed,
res.registered,
),
(expected_warnings, tests_passed, tests_failed, false)
);
}
Comment on lines +82 to +99
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Excess Number of Function Arguments
assertions.assert_build_result_failure has 5 arguments, max arguments = 4

Suppress


#[track_caller]
pub(super) fn assert_build_result_defaults(result: &BuildResult) {
assert_eq!(
(
result.validation_warnings.as_slice(),
result.tests_passed,
result.tests_failed,
result.registered,
),
([].as_slice(), 0, 0, false)
);
}

#[track_caller]
pub(super) fn assert_builder_config_defaults(config: &BuilderConfig) {
assert!(
config.max_iterations > 0
&& !config.timeout.is_zero()
&& config.timeout.as_secs() >= 60,
"defaults should provide a positive iteration cap and non-trivial timeout"
);
assert!(
config.validate_wasm && config.run_tests && config.auto_register,
"validation, tests, and registration should default to enabled"
);
assert!(
!config.cleanup_on_failure
&& config.wasm_output_dir.is_none()
&& config
.build_dir
.to_string_lossy()
.contains("ironclaw-builds"),
"cleanup, wasm output, and build directory defaults should be sensible"
);
}

#[track_caller]
pub(super) fn assert_optional_fields_none(req: &BuildRequirement) {
assert!(req.input_spec.is_none() && req.output_spec.is_none());
assert!(req.dependencies.is_empty() && req.capabilities.is_empty());
}

#[track_caller]
pub(super) fn assert_logs_contain_phase(logs: &[BuildLog], phase: BuildPhase) {
assert!(
logs.iter().any(|log| log.phase == phase),
Expand All @@ -39,6 +150,7 @@ mod assertions {
);
}

#[track_caller]
pub(super) fn assert_logs_message_contains(logs: &[BuildLog], needle: &str) {
assert!(
logs.iter().any(|log| log.message.contains(needle)
Expand All @@ -55,36 +167,32 @@ mod assertions {
}
}

#[test]
fn test_language_extension_all_variants() {
assert_eq!(Language::Rust.extension(), "rs");
assert_eq!(Language::Python.extension(), "py");
assert_eq!(Language::TypeScript.extension(), "ts");
assert_eq!(Language::JavaScript.extension(), "js");
assert_eq!(Language::Go.extension(), "go");
assert_eq!(Language::Bash.extension(), "sh");
#[rstest]
#[case(Language::Rust, "rs")]
#[case(Language::Python, "py")]
#[case(Language::TypeScript, "ts")]
#[case(Language::JavaScript, "js")]
#[case(Language::Go, "go")]
#[case(Language::Bash, "sh")]
fn test_language_extension_all_variants(#[case] language: Language, #[case] expected_ext: &str) {
assert_eq!(language.extension(), expected_ext);
}

#[test]
fn test_language_build_command_compiled_returns_some() {
#[rstest]
#[case(Language::Rust, "cargo", vec!["build", "--release"])]
#[case(Language::TypeScript, "npm", vec!["run", "build"])]
#[case(Language::Go, "go", vec!["build", "./..."])]
fn test_language_build_command_compiled_returns_some(
#[case] language: Language,
#[case] expected_program: &str,
#[case] expected_args: Vec<&str>,
) {
let dir = Path::new("/tmp/project");
let rust_cmd = Language::Rust.build_command(dir);
assert!(rust_cmd.is_some());
let rust_cmd = rust_cmd.expect("rust build command");
assert_eq!(rust_cmd.program, "cargo");
assert_eq!(rust_cmd.args, vec!["build", "--release"]);

let ts_cmd = Language::TypeScript.build_command(dir);
assert!(ts_cmd.is_some());
let ts_cmd = ts_cmd.expect("typescript build command");
assert_eq!(ts_cmd.program, "npm");
assert_eq!(ts_cmd.args, vec!["run", "build"]);

let go_cmd = Language::Go.build_command(dir);
assert!(go_cmd.is_some());
let go_cmd = go_cmd.expect("go build command");
assert_eq!(go_cmd.program, "go");
assert_eq!(go_cmd.args, vec!["build", "./..."]);
let cmd = language.build_command(dir);
assert!(cmd.is_some());
let cmd = cmd.expect("compiled language build command");
assert_eq!(cmd.program, expected_program);
assert_eq!(cmd.args, expected_args);
}

#[test]
Expand Down Expand Up @@ -138,18 +246,22 @@ fn test_language_test_command_all_variants_non_empty() {
}
}

#[test]
fn test_language_test_command_specific_tools() {
#[rstest]
#[case(Language::Rust, "cargo", vec!["test"])]
#[case(Language::Python, "python", vec!["-m", "pytest"])]
#[case(Language::TypeScript, "npm", vec!["test"])]
#[case(Language::JavaScript, "npm", vec!["test"])]
#[case(Language::Go, "go", vec!["test", "./..."])]
#[case(Language::Bash, "sh", vec!["-c", "shellcheck *.sh"])]
fn test_language_test_command_specific_tools(
#[case] language: Language,
#[case] expected_program: &str,
#[case] expected_args: Vec<&str>,
) {
let dir = Path::new("/tmp/p");
assert_eq!(Language::Rust.test_command(dir).program, "cargo");
assert_eq!(
Language::Python.test_command(dir).args,
vec!["-m", "pytest"]
);
assert_eq!(Language::TypeScript.test_command(dir).program, "npm");
assert_eq!(Language::JavaScript.test_command(dir).program, "npm");
assert_eq!(Language::Go.test_command(dir).args, vec!["test", "./..."]);
assert_eq!(Language::Bash.test_command(dir).program, "sh");
let cmd = language.test_command(dir);
assert_eq!(cmd.program, expected_program);
assert_eq!(cmd.args, expected_args);
}

#[test]
Expand Down Expand Up @@ -213,6 +325,8 @@ fn test_language_serde_roundtrip() {

#[test]
fn test_build_requirement_serde_roundtrip() {
use assertions::*;

let req = BuildRequirement {
name: ProjectName::new("my_tool").expect("valid project name"),
description: "A tool that does stuff".into(),
Expand All @@ -223,35 +337,13 @@ fn test_build_requirement_serde_roundtrip() {
dependencies: vec!["serde".into(), "reqwest".into()],
capabilities: vec!["http".into(), "workspace".into()],
};
let json = serde_json::to_string(&req).expect("serialize BuildRequirement");
let deserialized: BuildRequirement =
serde_json::from_str(&json).expect("deserialize BuildRequirement");
assert_eq!(
(
deserialized.name,
deserialized.description,
deserialized.software_type,
deserialized.language,
deserialized.input_spec,
deserialized.output_spec,
deserialized.dependencies,
deserialized.capabilities,
),
(
req.name,
req.description,
req.software_type,
req.language,
req.input_spec,
req.output_spec,
req.dependencies,
req.capabilities,
)
);
assert_build_requirement_roundtrip(&req);
}

#[test]
fn test_build_requirement_serde_optional_fields_none() {
use assertions::*;

let req = BuildRequirement {
name: ProjectName::new("minimal").expect("valid project name"),
description: "Bare minimum".into(),
Expand All @@ -265,32 +357,15 @@ fn test_build_requirement_serde_optional_fields_none() {
let json = serde_json::to_string(&req).expect("serialize BuildRequirement");
let deserialized: BuildRequirement =
serde_json::from_str(&json).expect("deserialize BuildRequirement");
assert!(deserialized.input_spec.is_none() && deserialized.output_spec.is_none());
assert!(deserialized.dependencies.is_empty() && deserialized.capabilities.is_empty());
assert_optional_fields_none(&deserialized);
}

#[test]
fn test_builder_config_default_sensible_values() {
use assertions::*;

let config = BuilderConfig::default();
assert!(
config.max_iterations > 0 && !config.timeout.is_zero() && config.timeout.as_secs() >= 60,
"defaults should provide a positive iteration cap and non-trivial timeout"
);
assert!(
config.validate_wasm && config.run_tests && config.auto_register,
"validation, tests, and registration should default to enabled"
);
assert!(
!config.cleanup_on_failure && config.wasm_output_dir.is_none(),
"cleanup should stay disabled and wasm_output_dir should default to None"
);
assert!(
config
.build_dir
.to_string_lossy()
.contains("ironclaw-builds"),
"build_dir should contain 'ironclaw-builds'"
);
assert_builder_config_defaults(&config);
}

#[test]
Expand Down Expand Up @@ -349,13 +424,7 @@ fn test_build_result_serde_success() {
};
let json = serde_json::to_string(&result).expect("serialize BuildResult");
let deserialized: BuildResult = serde_json::from_str(&json).expect("deserialize BuildResult");
assert_build_success(&deserialized);
assert_eq!(deserialized.iterations, 3);
assert_eq!(
(deserialized.tests_passed, deserialized.tests_failed),
(5, 0)
);
assert!(deserialized.registered);
assert_build_result_success(&deserialized);
}

#[test]
Expand Down Expand Up @@ -388,21 +457,20 @@ fn test_build_result_serde_failure() {
};
let json = serde_json::to_string(&result).expect("serialize BuildResult");
let deserialized: BuildResult = serde_json::from_str(&json).expect("deserialize BuildResult");
assert_build_failure_contains(&deserialized, "compilation error: undefined reference");
assert_eq!(deserialized.iterations, 10);
assert_eq!(
(
deserialized.validation_warnings.len(),
deserialized.tests_passed,
deserialized.tests_failed,
),
(1, 2, 3)
assert_build_result_failure(
&deserialized,
"compilation error: undefined reference",
1,
2,
3,
);
assert!(!deserialized.registered);
}

#[test]
fn test_build_result_default_fields_from_json() {
use assertions::*;

// Verify #[serde(default)] fields can be omitted in JSON
let json = serde_json::json!({
"build_id": "00000000-0000-0000-0000-000000000000",
Expand All @@ -426,10 +494,7 @@ fn test_build_result_default_fields_from_json() {
});
let result: BuildResult =
serde_json::from_value(json).expect("deserialize BuildResult from value");
assert_eq!(result.validation_warnings, Vec::<String>::new());
assert_eq!(result.tests_passed, 0);
assert_eq!(result.tests_failed, 0);
assert!(!result.registered);
assert_build_result_defaults(&result);
}

#[test]
Expand Down
Loading