From f7d456f7d4aaeb064d70b7e7f0e77759308b0868 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Sat, 20 Jun 2026 23:17:10 +0200 Subject: [PATCH 1/5] test(core): add integration tests verifying processor evaluates assertions Add three integration tests in processor/executor_tests.rs per PRD-3 testing decisions: - test_processor_evaluates_assertions_on_raw_executor_result: executor returns raw HttpResult with empty assertion_results; processor evaluates assertions from the request and populates them. - test_processor_evaluates_assertions_and_marks_failure: executor returns 404 but request expects status 200; processor marks result as failed. - test_processor_without_assertions_does_not_evaluate: no assertions on request; processor leaves assertion_results empty. All tests pass with the current processor-layer assertion evaluation. --- src/core/src/processor/executor_tests.rs | 138 +++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/src/core/src/processor/executor_tests.rs b/src/core/src/processor/executor_tests.rs index b9a8ea38..00ed7d46 100644 --- a/src/core/src/processor/executor_tests.rs +++ b/src/core/src/processor/executor_tests.rs @@ -2090,6 +2090,144 @@ GET https://api.example.com/final assert_eq!(res.files[0].failed_count, 1); } + #[test] + fn test_processor_evaluates_assertions_on_raw_executor_result() { + use crate::types::AssertionType; + + // The file has EXPECTED_RESPONSE_STATUS 200 which the parser converts + // into Assertion objects on the request. + let file_content = r#" +GET https://api.example.com/test +> EXPECTED_RESPONSE_STATUS 200 +"#; + let temp_file = create_temp_http_file(file_content); + let file_path = temp_file.path().to_str().unwrap().to_string(); + + // The mock executor returns a raw result with EMPTY assertion_results. + // The processor must evaluate the request's assertions and merge them. + let mock = MockHttpExecutor::new(vec![HttpResult { + request_name: None, + status_code: 200, + success: true, + error_message: None, + duration_ms: 1, + response_headers: None, + response_body: Some(r#"{"status":"ok"}"#.to_string()), + assertion_results: Vec::new(), // raw — no pre-evaluated assertions + }]); + + let result = process_http_files_with_executor( + &[file_path], + false, + None, + None, + false, + false, + &|req, v, i| mock.execute(req, v, i), + ); + + assert!(result.is_ok()); + let res = result.unwrap(); + assert!(res.success); + assert_eq!(res.files[0].success_count, 1); + + // The processed result contexts should have assertion_results populated + // by the processor (not by the mock). + let ctx = &res.files[0].result_contexts[0]; + let http_result = ctx.result.as_ref().expect("expected result"); + assert!( + !http_result.assertion_results.is_empty(), + "processor should have evaluated assertions, but assertion_results is empty" + ); + assert!( + http_result.assertion_results[0].passed, + "status assertion for 200 should pass" + ); + assert_eq!(http_result.assertion_results.len(), 1); + assert_eq!( + http_result.assertion_results[0].assertion.assertion_type, + AssertionType::Status + ); + assert_eq!( + http_result.assertion_results[0].assertion.expected_value, + "200" + ); + } + + #[test] + fn test_processor_evaluates_assertions_and_marks_failure() { + use crate::types::AssertionType; + + // Request expects status 200 but executor returns 404 + let file_content = r#" +GET https://api.example.com/not-found +> EXPECTED_RESPONSE_STATUS 200 +"#; + let temp_file = create_temp_http_file(file_content); + let file_path = temp_file.path().to_str().unwrap().to_string(); + + let mock = MockHttpExecutor::new(vec![HttpResult { + request_name: None, + status_code: 404, + success: false, + error_message: None, + duration_ms: 1, + response_headers: None, + response_body: None, + assertion_results: Vec::new(), // raw — no assertions pre-evaluated + }]); + + let result = process_http_files_with_executor( + &[file_path], + false, + None, + None, + false, + false, + &|req, v, i| mock.execute(req, v, i), + ); + + assert!(result.is_ok()); + let res = result.unwrap(); + // The request failed assertions, so overall result is failure + assert!(!res.success); + assert_eq!(res.files[0].failed_count, 1); + + let ctx = &res.files[0].result_contexts[0]; + let http_result = ctx.result.as_ref().expect("expected result"); + assert_eq!(http_result.assertion_results.len(), 1); + assert!(!http_result.assertion_results[0].passed); + assert!(!http_result.success); + } + + #[test] + fn test_processor_without_assertions_does_not_evaluate() { + let file_content = "GET https://api.example.com/test\n"; + let temp_file = create_temp_http_file(file_content); + let file_path = temp_file.path().to_str().unwrap().to_string(); + + let mock = MockHttpExecutor::new(vec![create_success_response(None)]); + + let result = process_http_files_with_executor( + &[file_path], + false, + None, + None, + false, + false, + &|req, v, i| mock.execute(req, v, i), + ); + + assert!(result.is_ok()); + let ctx = &result.unwrap().files[0].result_contexts[0]; + let http_result = ctx.result.as_ref().expect("expected result"); + assert!( + http_result.assertion_results.is_empty(), + "no assertions on request should leave assertion_results empty" + ); + assert!(http_result.success); + } + #[test] fn test_fail_fast_disabled_runs_all_requests() { let file_content = r#" From 39678ebe8e42c9657a0b4edab8e17e42ea90b479 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Sat, 20 Jun 2026 23:17:50 +0200 Subject: [PATCH 2/5] fix(core): remove unused import from processor integration tests --- src/core/src/processor/executor_tests.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/src/processor/executor_tests.rs b/src/core/src/processor/executor_tests.rs index 00ed7d46..926e70dc 100644 --- a/src/core/src/processor/executor_tests.rs +++ b/src/core/src/processor/executor_tests.rs @@ -2156,8 +2156,6 @@ GET https://api.example.com/test #[test] fn test_processor_evaluates_assertions_and_marks_failure() { - use crate::types::AssertionType; - // Request expects status 200 but executor returns 404 let file_content = r#" GET https://api.example.com/not-found From 8a86683a2f78db1b51d66e953c3175d2c719ec9b Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Sat, 20 Jun 2026 23:18:55 +0200 Subject: [PATCH 3/5] refactor(core): remove assertion evaluation from processor layer Temporarily remove assertion evaluation from process_single_request and process_requests_incremental. This is the RED phase of TDD: - Without assertion evaluation in the processor, the integration tests test_processor_evaluates_assertions_on_raw_executor_result and test_processor_evaluates_assertions_and_marks_failure now fail. The runner was already decoupled (returns Vec::new() for assertion_results). This commit completes the separation: assertion evaluation will be re-added to the processor layer only, verifying the tests catch the missing behavior. --- src/core/src/processor/executor.rs | 12 +----------- src/core/src/processor/incremental_loop.rs | 10 +--------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/core/src/processor/executor.rs b/src/core/src/processor/executor.rs index e475736a..35c52bbf 100644 --- a/src/core/src/processor/executor.rs +++ b/src/core/src/processor/executor.rs @@ -3,7 +3,6 @@ use super::output; use crate::request_substitution::{ substitute_functions_in_request, substitute_request_variables_in_request, }; -use crate::assertions; use crate::colors; use crate::conditions; use crate::logging::Log; @@ -235,16 +234,7 @@ where config.verbose || config.fail_fast, config.insecure, ) { - Ok(mut result) => { - if !processed_request.assertions.is_empty() { - let assertion_results = - assertions::evaluate_assertions(&processed_request.assertions, &result); - let all_passed = assertion_results.iter().all(|r| r.passed); - result.success = all_passed; - result.assertion_results = assertion_results; - } - Ok((RequestProcessResult::Completed(result), processed_request)) - } + Ok(result) => Ok((RequestProcessResult::Completed(result), processed_request)), Err(e) => { output::log_execution_error(&processed_request, &e, log, config.include_secrets); Ok((RequestProcessResult::ExecutionError, processed_request)) diff --git a/src/core/src/processor/incremental_loop.rs b/src/core/src/processor/incremental_loop.rs index 8f312226..658a7429 100644 --- a/src/core/src/processor/incremental_loop.rs +++ b/src/core/src/processor/incremental_loop.rs @@ -1,4 +1,3 @@ -use crate::assertions; use crate::conditions; use crate::request_substitution::{ substitute_functions_in_request, substitute_request_variables_in_request, @@ -293,14 +292,7 @@ where // Clone the request for the executor so the original remains available // for the callback and context tracking. match executor(request.clone(), false, insecure).await { - Ok(mut result) => { - if !request.assertions.is_empty() { - let assertion_results = - assertions::evaluate_assertions(&request.assertions, &result); - let all_passed = assertion_results.iter().all(|r| r.passed); - result.success = all_passed; - result.assertion_results = assertion_results; - } + Ok(result) => { add_request_context( &mut request_contexts, request.clone(), From b679ffb6ec93fb69ccd3c669a965728ed47e4d53 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Sat, 20 Jun 2026 23:19:24 +0200 Subject: [PATCH 4/5] feat(core): add assertion evaluation back to processor layer GREEN phase of TDD: re-add assertion evaluation to process_single_request and process_requests_incremental. The processor now: 1. After receiving the HttpResult from the executor, checks if the request has assertions. 2. Calls assertions::evaluate_assertions to evaluate them against the result. 3. Sets result.success to the AND of all assertion results. 4. Sets result.assertion_results to the evaluated Vec. This completes the decoupling: the runner returns raw HttpResult with empty assertion_results, and the processor handles assertion evaluation. The integration tests test_processor_evaluates_assertions_on_raw_executor_result and test_processor_evaluates_assertions_and_marks_failure now pass. --- src/core/src/processor/executor.rs | 12 +++++++++++- src/core/src/processor/incremental_loop.rs | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/core/src/processor/executor.rs b/src/core/src/processor/executor.rs index 35c52bbf..d62c4ca8 100644 --- a/src/core/src/processor/executor.rs +++ b/src/core/src/processor/executor.rs @@ -3,6 +3,7 @@ use super::output; use crate::request_substitution::{ substitute_functions_in_request, substitute_request_variables_in_request, }; +use crate::assertions; use crate::colors; use crate::conditions; use crate::logging::Log; @@ -234,7 +235,16 @@ where config.verbose || config.fail_fast, config.insecure, ) { - Ok(result) => Ok((RequestProcessResult::Completed(result), processed_request)), + Ok(mut result) => { + if !processed_request.assertions.is_empty() { + let assertion_results = + assertions::evaluate_assertions(&processed_request.assertions, &result); + let all_passed = assertion_results.iter().all(|r| r.passed); + result.success = all_passed; + result.assertion_results = assertion_results; + } + Ok((RequestProcessResult::Completed(result), processed_request)) + }, Err(e) => { output::log_execution_error(&processed_request, &e, log, config.include_secrets); Ok((RequestProcessResult::ExecutionError, processed_request)) diff --git a/src/core/src/processor/incremental_loop.rs b/src/core/src/processor/incremental_loop.rs index 658a7429..8f312226 100644 --- a/src/core/src/processor/incremental_loop.rs +++ b/src/core/src/processor/incremental_loop.rs @@ -1,3 +1,4 @@ +use crate::assertions; use crate::conditions; use crate::request_substitution::{ substitute_functions_in_request, substitute_request_variables_in_request, @@ -292,7 +293,14 @@ where // Clone the request for the executor so the original remains available // for the callback and context tracking. match executor(request.clone(), false, insecure).await { - Ok(result) => { + Ok(mut result) => { + if !request.assertions.is_empty() { + let assertion_results = + assertions::evaluate_assertions(&request.assertions, &result); + let all_passed = assertion_results.iter().all(|r| r.passed); + result.success = all_passed; + result.assertion_results = assertion_results; + } add_request_context( &mut request_contexts, request.clone(), From 48431fc5b2f59c87734cec01016f8cb9ccd62efb Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Sat, 20 Jun 2026 23:21:46 +0200 Subject: [PATCH 5/5] test(core): add integration tests for assertion evaluation in incremental path Add test_incremental_evaluates_assertions_on_raw_executor_result and test_incremental_evaluates_assertions_and_marks_failure to verify that the incremental processing path (process_requests_incremental) evaluates assertions when the executor returns raw results with empty assertion_results. This ensures both the sync processor path (process_single_request) and the async-ready incremental path (process_requests_incremental) correctly evaluate assertions, completing the assertion decoupling coverage. --- src/core/src/processor/incremental_tests.rs | 112 ++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/core/src/processor/incremental_tests.rs b/src/core/src/processor/incremental_tests.rs index d681d63b..d91e5361 100644 --- a/src/core/src/processor/incremental_tests.rs +++ b/src/core/src/processor/incremental_tests.rs @@ -441,6 +441,118 @@ GET https://api.example.com/json } } +#[test] +fn test_incremental_evaluates_assertions_on_raw_executor_result() { + use crate::types::AssertionType; + + // Use EXPECTED_RESPONSE_STATUS which IS parsed by the parser. + let file_content = r#" +GET https://api.example.com/test +> EXPECTED_RESPONSE_STATUS 200 +"#; + let temp_file = create_temp_http_file(file_content); + let file_path = temp_file.path().to_str().unwrap(); + + let captured = Arc::new(Mutex::new(None::)); + let captured_clone = Arc::clone(&captured); + + // Mock returns raw result (empty assertion_results). The processor + // must evaluate the request's assertions and merge them. + let raw_result = HttpResult { + request_name: None, + status_code: 200, + success: true, + error_message: None, + duration_ms: 1, + response_headers: None, + response_body: Some(r#"{"status":"ok"}"#.to_string()), + assertion_results: Vec::new(), + }; + let mock = MockHttpExecutor::new(vec![raw_result]); + + let _ = process_http_file_incremental_with_executor( + file_path, + None, + false, + 0, + move |_idx, _total, result| { + if let RequestProcessingResult::Executed { result, .. } = result { + *captured_clone.lock().unwrap() = Some(result); + } + false + }, + &|req, v, i| mock.execute(req, v, i), + ); + + let http_result = captured.lock().unwrap().take() + .expect("expected Executed result"); + + assert!( + !http_result.assertion_results.is_empty(), + "processor should have evaluated assertions in incremental path" + ); + assert_eq!(http_result.assertion_results.len(), 1); + assert!( + http_result.assertion_results[0].passed, + "status assertion for 200 should pass" + ); + assert_eq!( + http_result.assertion_results[0].assertion.assertion_type, + AssertionType::Status + ); + assert_eq!( + http_result.assertion_results[0].assertion.expected_value, + "200" + ); +} + +#[test] +fn test_incremental_evaluates_assertions_and_marks_failure() { + // Request expects status 200 but executor returns 404 + let file_content = r#" +GET https://api.example.com/not-found +> EXPECTED_RESPONSE_STATUS 200 +"#; + let temp_file = create_temp_http_file(file_content); + let file_path = temp_file.path().to_str().unwrap(); + + let captured = Arc::new(Mutex::new(None::)); + let captured_clone = Arc::clone(&captured); + + let raw_result = HttpResult { + request_name: None, + status_code: 404, + success: false, + error_message: None, + duration_ms: 1, + response_headers: None, + response_body: None, + assertion_results: Vec::new(), + }; + let mock = MockHttpExecutor::new(vec![raw_result]); + + let _ = process_http_file_incremental_with_executor( + file_path, + None, + false, + 0, + move |_idx, _total, result| { + if let RequestProcessingResult::Executed { result, .. } = result { + *captured_clone.lock().unwrap() = Some(result); + } + false + }, + &|req, v, i| mock.execute(req, v, i), + ); + + let http_result = captured.lock().unwrap().take() + .expect("expected Executed result"); + + assert_eq!(http_result.assertion_results.len(), 1); + assert!(!http_result.assertion_results[0].passed); + assert!(!http_result.success); +} + #[test] fn test_multiple_requests_with_mixed_results() { let file_content = r#"