use super::*;
use crate::llm::api::LlmRequestPayload;
fn text_mock(text: &str) -> LlmMock {
LlmMock {
text: text.to_string(),
tool_calls: Vec::new(),
raw_tool_calls: Vec::new(),
match_pattern: None,
scope: DEFAULT_MOCK_SCOPE.to_string(),
entry_id: String::new(),
sticky: false,
input_tokens: None,
output_tokens: None,
cache_read_tokens: None,
cache_write_tokens: None,
thinking: None,
thinking_summary: None,
stop_reason: None,
model: "fixture-model".to_string(),
provider: None,
blocks: None,
logprobs: Vec::new(),
error: None,
stream_chunks: Vec::new(),
}
}
#[test]
fn build_mock_result_surfaces_fixture_tool_calls() {
let mock = crate::llm::jsonl::parse_llm_mock_value(&serde_json::json!({
"match": "*",
"consume_match": true,
"tool_calls": [{"name": "ask_user", "arguments": {"question": "Which?"}}]
}))
.expect("parse mock");
assert!(
!mock.tool_calls.is_empty(),
"fixture tool_calls must parse into the mock: {:?}",
mock.tool_calls
);
let result = build_mock_result(&mock, 10);
assert!(
!result.tool_calls.is_empty(),
"build_mock_result must surface tool_calls: {:?}",
result.tool_calls
);
assert_eq!(result.tool_calls[0]["name"], "ask_user");
}
#[test]
fn cli_llm_mock_replay_scope_survives_provider_worker_thread() {
reset_llm_mock_state();
install_cli_llm_mocks(vec![text_mock("cross-thread replay")]);
let request = LlmRequestPayload::from(&crate::llm::api::options::base_opts("anthropic"));
assert!(request.cli_llm_mock_scope.is_some());
assert!(crate::llm::providers::MockProvider::should_intercept_request(&request));
let result = std::thread::spawn(move || {
assert!(crate::llm::providers::MockProvider::should_intercept_request(&request));
mock_llm_response(&request)
})
.join()
.expect("provider worker thread")
.expect("mock response");
assert_eq!(result.text, "cross-thread replay");
clear_cli_llm_mock_mode();
}
#[test]
fn cli_llm_mock_record_scope_collects_provider_worker_thread_results() {
reset_llm_mock_state();
enable_cli_llm_mock_recording();
let request = LlmRequestPayload::from(&crate::llm::api::options::base_opts("anthropic"));
let result = build_mock_result(&text_mock("cross-thread record"), 7);
assert!(request.cli_llm_mock_scope.is_some());
std::thread::spawn(move || record_cli_llm_result(&request, &result))
.join()
.expect("provider worker thread");
let recordings = take_cli_llm_recordings();
assert_eq!(recordings.len(), 1);
assert_eq!(recordings[0].text, "cross-thread record");
clear_cli_llm_mock_mode();
}
#[test]
fn cli_mock_native_tool_calls_reach_the_live_result() {
reset_llm_mock_state();
let mocks = vec![crate::llm::jsonl::parse_llm_mock_value(&serde_json::json!({
"match": "*",
"consume_match": true,
"tool_calls": [{"name": "ask_user", "arguments": {"question": "Which?"}}]
}))
.expect("parse mock")];
install_cli_llm_mocks(mocks);
let request = LlmRequestPayload::from(&crate::llm::api::options::base_opts("fixture"));
assert!(
request.cli_llm_mock_scope.is_some(),
"cli mock scope must be active"
);
let result = mock_llm_response(&request).expect("mock response");
clear_cli_llm_mock_mode();
assert!(
!result.tool_calls.is_empty(),
"CLI mock native tool_calls must reach the live result: text={:?} tool_calls={:?}",
result.text,
result.tool_calls
);
}
fn request_with_scope(prompt: &str, scope: Option<&str>) -> LlmRequestPayload {
let mut opts = crate::llm::api::options::base_opts("fixture");
opts.messages = vec![serde_json::json!({"role": "user", "content": prompt})];
opts.mock_scope = scope.map(str::to_string);
LlmRequestPayload::from(&opts)
}
fn v1_fixture(strict_scopes: bool, entries: &[serde_json::Value]) -> LlmMockFixture {
let mocks = entries
.iter()
.enumerate()
.map(|(idx, value)| {
crate::llm::jsonl::parse_llm_mock_value_versioned(value, 1, idx)
.expect("parse v1 fixture entry")
})
.collect();
LlmMockFixture {
schema_version: 1,
strict_scopes,
mocks,
}
}
#[test]
fn scoped_fixture_serves_main_and_judge_from_their_own_buckets() {
reset_llm_mock_state();
install_cli_llm_mock_fixture(v1_fixture(
false,
&[
serde_json::json!({"scope": "main", "text": "MAIN"}),
serde_json::json!({"scope": "judge", "text": "JUDGE"}),
],
));
let main = mock_llm_response(&request_with_scope("turn", Some("main"))).expect("main");
assert_eq!(main.text, "MAIN");
let judge = mock_llm_response(&request_with_scope("verify", Some("judge"))).expect("judge");
assert_eq!(judge.text, "JUDGE");
let receipts = get_llm_mock_receipts();
assert_eq!(receipts.len(), 2);
assert_eq!(receipts[0].scope, "main");
assert!(receipts[0].matched);
assert_eq!(receipts[1].scope, "judge");
clear_cli_llm_mock_mode();
}
#[test]
fn aux_call_falls_through_to_default_never_to_main() {
reset_llm_mock_state();
install_cli_llm_mock_fixture(v1_fixture(
false,
&[
serde_json::json!({"scope": "main", "text": "MAIN"}),
serde_json::json!({"scope": "default", "text": "DEFAULT"}),
],
));
let judge = mock_llm_response(&request_with_scope("verify", Some("judge"))).expect("judge");
assert_eq!(
judge.text, "DEFAULT",
"unscoped-aux call must reach default"
);
let main = mock_llm_response(&request_with_scope("turn", Some("main"))).expect("main");
assert_eq!(main.text, "MAIN");
let receipts = get_llm_mock_receipts();
assert_eq!(
receipts[0].scope, "default",
"judge drew from default bucket"
);
assert!(receipts[0].matched);
clear_cli_llm_mock_mode();
}
#[test]
fn v0_unscoped_fixture_replays_in_fifo_order() {
reset_llm_mock_state();
let mocks = vec![
crate::llm::jsonl::parse_llm_mock_value(&serde_json::json!({"text": "first"}))
.expect("parse first"),
crate::llm::jsonl::parse_llm_mock_value(&serde_json::json!({"text": "second"}))
.expect("parse second"),
];
install_cli_llm_mocks(mocks);
assert_eq!(
mock_llm_response(&request_with_scope("a", None))
.expect("first")
.text,
"first"
);
assert_eq!(
mock_llm_response(&request_with_scope("b", None))
.expect("second")
.text,
"second"
);
clear_cli_llm_mock_mode();
}
#[test]
fn sticky_entry_reused_while_once_entry_is_consumed() {
reset_llm_mock_state();
install_cli_llm_mock_fixture(v1_fixture(
false,
&[
serde_json::json!({"scope": "judge", "match": "*", "consume": "sticky", "text": "STICKY"}),
serde_json::json!({"scope": "main", "text": "ONCE"}),
],
));
for _ in 0..3 {
assert_eq!(
mock_llm_response(&request_with_scope("q", Some("judge")))
.expect("sticky")
.text,
"STICKY"
);
}
assert_eq!(
mock_llm_response(&request_with_scope("t", Some("main")))
.expect("once")
.text,
"ONCE"
);
assert!(
mock_llm_response(&request_with_scope("t2", Some("main"))).is_err(),
"a consumed once-entry must not replay"
);
clear_cli_llm_mock_mode();
}
#[test]
fn strict_scopes_makes_unscoped_aux_a_hard_miss() {
reset_llm_mock_state();
install_cli_llm_mock_fixture(v1_fixture(
true,
&[serde_json::json!({"scope": "default", "text": "DEFAULT"})],
));
assert!(
mock_llm_response(&request_with_scope("verify", Some("judge"))).is_err(),
"strict scopes must make an unscoped-aux call a hard miss"
);
let receipts = get_llm_mock_receipts();
assert!(
receipts.iter().any(|r| r.scope == "judge" && !r.matched),
"the hard miss must be recorded as an unmatched receipt: {receipts:?}"
);
let def = mock_llm_response(&request_with_scope("x", None)).expect("default");
assert_eq!(def.text, "DEFAULT");
clear_cli_llm_mock_mode();
}