mod common;
use predicates::prelude::*;
use std::sync::atomic::Ordering;
use tempfile::tempdir;
#[tokio::test]
async fn compile_loop_diff_anthropic_mock_iterates_until_pass() {
let (base_url, call_count, ct) =
common::compile_loop_diff_anthropic_mock::spawn_compile_loop_diff_anthropic_mock_server()
.await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let url_clone = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("target.lua");
common::agent_block_cmd()
.args([
"-s",
&common::fixture("compile_loop_diff_anthropic_mock.lua"),
])
.env("ANTHROPIC_BASE_URL_TEST", &url_clone)
.env(
"COMPILE_LOOP_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("AGENT_BLOCK_HOME", tmp.path())
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_DIFF_MOCK_PASS"));
})
.await
.expect("subprocess assertion task should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
2,
"expected exactly 2 HTTP calls to the diff anthropic mock (iter1: apply-fail, iter2: pass)"
);
ct.cancel();
}
#[tokio::test]
async fn compile_loop_openai_mock_iterates_until_pass() {
let (base_url, call_count, ct) =
common::compile_loop_openai_mock::spawn_compile_loop_openai_mock_server().await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let url_clone = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("target.lua");
common::agent_block_cmd()
.args(["-s", &common::fixture("compile_loop_openai_mock.lua")])
.env("OPENAI_BASE_URL_TEST", &url_clone)
.env(
"COMPILE_LOOP_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_MOCK_PASS"));
})
.await
.expect("subprocess assertion task should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
2,
"expected exactly 2 HTTP calls to the mock (turn 1: broken, turn 2: fixed)"
);
ct.cancel();
}
#[tokio::test]
async fn compile_loop_anthropic_mock_iterates_until_pass() {
let (base_url, call_count, ct) =
common::compile_loop_anthropic_mock::spawn_compile_loop_anthropic_mock_server().await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let url_clone = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("target.lua");
common::agent_block_cmd()
.args(["-s", &common::fixture("compile_loop_anthropic_mock.lua")])
.env("ANTHROPIC_BASE_URL_TEST", &url_clone)
.env(
"COMPILE_LOOP_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("AGENT_BLOCK_HOME", tmp.path())
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_MOCK_PASS"));
})
.await
.expect("subprocess assertion task should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
2,
"expected exactly 2 HTTP calls to the anthropic mock"
);
ct.cancel();
}
#[tokio::test]
async fn compile_loop_diff_multi_anthropic_mock_iterates_until_pass() {
let (base_url, call_count, ct) =
common::compile_loop_diff_multi_anthropic_mock::spawn_compile_loop_diff_multi_anthropic_mock_server()
.await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let url_clone = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let file_a = tmp.path().join("file_a.lua");
let file_b = tmp.path().join("file_b.lua");
common::agent_block_cmd()
.args([
"-s",
&common::fixture("compile_loop_diff_multi_anthropic_mock.lua"),
])
.env("ANTHROPIC_BASE_URL_TEST", &url_clone)
.env(
"COMPILE_LOOP_TARGET_FILES",
format!(
"{}:{}",
file_a.to_str().expect("utf8 path"),
file_b.to_str().expect("utf8 path")
),
)
.env("AGENT_BLOCK_HOME", tmp.path())
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains(
"COMPILE_LOOP_DIFF_MULTI_MOCK_PASS",
));
})
.await
.expect("subprocess assertion task should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
1,
"expected exactly 1 HTTP call to the multi diff mock (happy path: 2 files in 1 turn)"
);
ct.cancel();
}
#[tokio::test]
async fn compile_loop_diff_multi_anthropic_mock_two_iter_converges() {
let (base_url, call_count, ct) =
common::compile_loop_diff_multi_anthropic_mock::spawn_compile_loop_diff_multi_anthropic_mock_two_iter_server()
.await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let url_clone = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let file_a = tmp.path().join("file_a.lua");
let file_b = tmp.path().join("file_b.lua");
common::agent_block_cmd()
.args([
"-s",
&common::fixture("compile_loop_diff_multi_anthropic_mock_two_iter.lua"),
])
.env("ANTHROPIC_BASE_URL_TEST", &url_clone)
.env(
"COMPILE_LOOP_TARGET_FILES",
format!(
"{}:{}",
file_a.to_str().expect("utf8 path"),
file_b.to_str().expect("utf8 path")
),
)
.env("AGENT_BLOCK_HOME", tmp.path())
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains(
"COMPILE_LOOP_DIFF_MULTI_MOCK_TWO_ITER_PASS",
));
})
.await
.expect("subprocess assertion task should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
2,
"expected exactly 2 HTTP calls to the multi diff mock (iter1: apply-fail, iter2: pass)"
);
ct.cancel();
}
#[tokio::test]
async fn compile_loop_distill_openai_mock_iterates_until_pass() {
let (addr, state) = common::compile_loop_distill_mock::spawn_distill_mock("openai").await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let base_url = format!("http://{addr}");
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("distill_target.lua");
common::agent_block_cmd()
.args(["-s", &common::fixture("compile_loop_distill_mock.lua")])
.env("OPENAI_BASE_URL_TEST", &base_url)
.env("DISTILL_MOCK_PROVIDER", "openai")
.env(
"COMPILE_LOOP_DISTILL_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_DISTILL_MOCK_PASS"));
})
.await
.expect("subprocess assertion task should not panic");
assert!(
state.distill_call_count.load(Ordering::SeqCst) > 0,
"distill_call_count must be > 0: distill subloop was not triggered"
);
let body_guard = state.received_distill_body.lock().unwrap();
let distill_body = body_guard
.as_ref()
.expect("received_distill_body must be set after distill call");
assert!(
distill_body.get("tools").is_none(),
"BC5: distill LLM call must not include `tools` field in request body"
);
}
#[tokio::test]
async fn compile_loop_distill_anthropic_mock_iterates_until_pass() {
let (addr, state) = common::compile_loop_distill_mock::spawn_distill_mock("anthropic").await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let base_url = format!("http://{addr}");
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("distill_target.lua");
common::agent_block_cmd()
.args(["-s", &common::fixture("compile_loop_distill_mock.lua")])
.env("ANTHROPIC_BASE_URL_TEST", &base_url)
.env("DISTILL_MOCK_PROVIDER", "anthropic")
.env(
"COMPILE_LOOP_DISTILL_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_DISTILL_MOCK_PASS"));
})
.await
.expect("subprocess assertion task should not panic");
assert!(
state.distill_call_count.load(Ordering::SeqCst) > 0,
"distill_call_count must be > 0: distill subloop was not triggered"
);
let body_guard = state.received_distill_body.lock().unwrap();
let distill_body = body_guard
.as_ref()
.expect("received_distill_body must be set after distill call");
assert!(
distill_body.get("tools").is_none(),
"BC5: distill LLM call must not include `tools` field in request body"
);
}
#[tokio::test]
async fn compile_loop_read_file_range_verbatim() {
let (addr, state) = common::compile_loop_distill_mock::spawn_range_mock().await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let base_url = format!("http://{addr}");
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("range_target.lua");
common::agent_block_cmd()
.args([
"-s",
&common::fixture("compile_loop_distill_range_mock.lua"),
])
.env("ANTHROPIC_BASE_URL_TEST", &base_url)
.env(
"COMPILE_LOOP_RANGE_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("READ_FILE_RANGE_VERBATIM_PASS"));
})
.await
.expect("subprocess assertion task should not panic");
assert_eq!(
state.call_count.load(Ordering::SeqCst),
2,
"expected exactly 2 HTTP calls to the range mock (turn 0: read_file_range, turn 1: SR pass)"
);
assert_eq!(
state.distill_call_count.load(Ordering::SeqCst),
0,
"range mock must not trigger distill subloop (read_file_range bypasses distill)"
);
}
#[tokio::test]
async fn compile_loop_openai_mock_three_turn_converges() {
let (base_url, call_count, ct) = common::compile_loop_openai_mock_three_turn::spawn_compile_loop_openai_mock_three_turn_server().await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let url_clone = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("target.lua");
common::agent_block_cmd()
.args([
"-s",
&common::fixture("compile_loop_openai_mock_three_turn.lua"),
])
.env("OPENAI_BASE_URL_TEST", &url_clone)
.env(
"COMPILE_LOOP_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_MOCK_PASS"));
})
.await
.expect("subprocess assertion task (run 1) should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
3,
"run 1: expected exactly 3 HTTP calls to the 3-turn mock (broken1, broken2, fixed)"
);
call_count.store(0, Ordering::SeqCst);
let url_clone2 = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("target.lua");
common::agent_block_cmd()
.args([
"-s",
&common::fixture("compile_loop_openai_mock_three_turn.lua"),
])
.env("OPENAI_BASE_URL_TEST", &url_clone2)
.env(
"COMPILE_LOOP_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("RUST_LOG", "off")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_MOCK_PASS"));
})
.await
.expect("subprocess assertion task (run 2) should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
3,
"run 2: expected exactly 3 HTTP calls to the 3-turn mock (broken1, broken2, fixed)"
);
ct.cancel();
}
#[tokio::test]
async fn compile_loop_anthropic_mock_emits_obs_events() {
let (base_url, call_count, ct) =
common::compile_loop_anthropic_mock::spawn_compile_loop_anthropic_mock_server().await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let url_clone = base_url.clone();
tokio::task::spawn_blocking(move || {
let tmp = tempdir().expect("tempdir");
let target_file = tmp.path().join("target.lua");
common::agent_block_cmd()
.args(["-s", &common::fixture("compile_loop_anthropic_mock.lua")])
.env("ANTHROPIC_BASE_URL_TEST", &url_clone)
.env(
"COMPILE_LOOP_TARGET",
target_file.to_str().expect("utf8 path"),
)
.env("AGENT_BLOCK_HOME", tmp.path())
.env("RUST_LOG", "info")
.env("AGENT_BLOCK_LLM_DUMP", "meta")
.assert()
.success()
.stdout(predicate::str::contains("COMPILE_LOOP_MOCK_PASS"))
.stdout(predicate::str::contains(
"prefix=ab.obs event=iter_start component=compile_loop",
))
.stdout(predicate::str::contains(
"prefix=ab.obs event=iter_result component=compile_loop",
))
.stdout(predicate::str::contains(
"prefix=ab.obs event=converged component=compile_loop",
));
})
.await
.expect("subprocess assertion task should not panic");
assert_eq!(
call_count.load(Ordering::SeqCst),
2,
"expected exactly 2 HTTP calls to the anthropic mock"
);
ct.cancel();
}