mod common;
use std::collections::BTreeMap;
use common::{REVIEW_CYCLES, factory, prompts};
use layover_core::prompt::resolve;
use layover_core::{Itinerary, ItineraryId, PipelineName, Schedule, Trigger};
#[test]
fn feature_work_is_manual_and_review_work_is_scheduled() {
let config = factory();
assert_eq!(
config.pipelines[&PipelineName::from("development")].trigger,
Trigger::Manual
);
let review = &config.pipelines[&PipelineName::from("review-bot")];
assert_eq!(
review.trigger,
Trigger::Scheduled(Schedule::Every(std::time::Duration::from_secs(3_600))),
"the review bot runs hourly"
);
assert_eq!(review.entry.as_str(), "pr_scanner");
}
#[test]
fn both_pipelines_are_entry_points_and_nothing_else_is() {
let config = factory();
let mut entries: Vec<String> = config.entry_agents().map(ToString::to_string).collect();
entries.sort();
assert_eq!(entries, ["analyst", "follower", "pr_scanner"]);
}
#[test]
fn the_hourly_schedule_is_slower_than_a_run_may_take() {
let config = factory();
let interval = config.pipelines[&PipelineName::from("review-bot")]
.trigger
.schedule()
.and_then(Schedule::interval)
.expect("the review bot has a fixed interval");
assert!(
interval.as_secs() >= config.defaults.timeout_sec,
"an hourly schedule must outlast the {}s run timeout",
config.defaults.timeout_sec
);
}
#[test]
fn every_pipeline_declares_the_flags_its_prompts_test() {
let config = factory();
let mut declared: Vec<&str> = config.declared_flags().collect();
declared.sort_unstable();
declared.dedup();
assert_eq!(declared, ["deep_analysis", "draft_pr", "run_e2e"]);
}
#[test]
fn a_typo_at_trigger_time_is_refused_rather_than_ignored() {
let config = factory();
let pipeline = &config.pipelines[&PipelineName::from("development")];
let overrides = BTreeMap::from([("run_e2ee".to_owned(), true)]);
assert!(
pipeline.flags_for_run(&overrides).is_err(),
"an undeclared flag must not silently do nothing"
);
}
#[test]
fn the_tester_prompt_swaps_branches_on_the_e2e_flag() {
let config = factory();
let pipeline = &config.pipelines[&PipelineName::from("development")];
let source = prompts();
let off = pipeline
.flags_for_run(&BTreeMap::new())
.expect("defaults are always valid");
let on = pipeline
.flags_for_run(&BTreeMap::from([("run_e2e".to_owned(), true)]))
.expect("declared flag");
let without = resolve(&source, "tester.md", &off).expect("resolves with the flag off");
let with = resolve(&source, "tester.md", &on).expect("resolves with the flag on");
assert!(without.contains("Local suite only"));
assert!(!without.contains("End-to-end suite"));
assert!(with.contains("End-to-end suite"));
assert!(!with.contains("Local suite only"));
for text in [&without, &with] {
assert!(text.contains("You are the tester."));
assert!(text.contains("explicit verdict"));
assert!(
!text.contains("@include"),
"an agent must never see Layover's own directive syntax"
);
}
}
#[test]
fn the_publisher_opens_a_draft_by_default() {
let config = factory();
let pipeline = &config.pipelines[&PipelineName::from("development")];
let flags = pipeline
.flags_for_run(&BTreeMap::new())
.expect("defaults are always valid");
assert_eq!(flags.get("draft_pr"), Some(true));
let text = resolve(&prompts(), "publisher.md", &flags).expect("resolves");
assert!(text.contains("Draft pull request"));
}
#[test]
fn an_agent_with_no_conditional_sections_resolves_to_its_file() {
let config = factory();
let flags = config.pipelines[&PipelineName::from("development")]
.flags_for_run(&BTreeMap::new())
.expect("defaults are always valid");
let text = resolve(&prompts(), "reviewer.md", &flags).expect("resolves");
assert!(text.contains("You are the reviewer."));
assert!(!text.contains("@include"));
}
fn fly(itinerary: &Itinerary, hops: &mut u32) -> bool {
match itinerary.authorize_send(*hops) {
Ok(next) => {
*hops = next;
true
}
Err(_) => false,
}
}
fn publishable_review_cycles(max_hops: u32, lead_in: u32) -> u32 {
let itinerary = Itinerary::new(ItineraryId::generate(), max_hops, 1_000.0, 10_000);
let mut hops = itinerary.initial_hops();
for _ in 0..lead_in - 1 {
if !fly(&itinerary, &mut hops) {
return 0;
}
}
let mut publishable = 0;
for cycle in 1..=100 {
if !fly(&itinerary, &mut hops) || !fly(&itinerary, &mut hops) {
return publishable;
}
if itinerary.authorize_send(hops).is_err() {
return publishable;
}
publishable = cycle;
}
publishable
}
const MANUAL_LEAD_IN: u32 = 4;
const SCHEDULED_LEAD_IN: u32 = 5;
#[test]
fn the_configured_hop_budget_allows_eight_cycles_from_either_entry() {
let config = factory();
assert_eq!(config.defaults.max_hops, 22);
assert_eq!(
publishable_review_cycles(config.defaults.max_hops, MANUAL_LEAD_IN),
REVIEW_CYCLES,
"manual path: 2N + 5 flights"
);
assert_eq!(
publishable_review_cycles(config.defaults.max_hops, SCHEDULED_LEAD_IN),
REVIEW_CYCLES,
"scheduled path: 2N + 6 flights, which is what 22 is sized for"
);
}
#[test]
fn the_default_hop_budget_leaves_no_room_to_fix_anything() {
assert_eq!(
publishable_review_cycles(8, MANUAL_LEAD_IN),
1,
"the default budget permits the happy path and nothing else"
);
assert_eq!(
publishable_review_cycles(8, SCHEDULED_LEAD_IN),
1,
"and the scheduled path is one flight worse off still"
);
}
#[test]
fn the_hop_budget_is_the_documented_function_of_the_cycle_count() {
for cycles in 1..=REVIEW_CYCLES {
for (lead_in, fixed) in [(MANUAL_LEAD_IN, 5), (SCHEDULED_LEAD_IN, 6)] {
let minimum = 2 * cycles + fixed;
assert_eq!(
publishable_review_cycles(minimum, lead_in),
cycles,
"{minimum} hops should buy {cycles} cycles on the lead-in-{lead_in} path"
);
assert_eq!(
publishable_review_cycles(minimum - 1, lead_in),
cycles - 1,
"one hop short must cost exactly one cycle"
);
}
}
}
#[test]
fn the_run_cap_covers_the_whole_loop_without_relying_on_cost_reporting() {
let config = factory();
let mut itinerary = Itinerary::new(
ItineraryId::generate(),
config.defaults.max_hops,
config.defaults.fuel_usd,
config.defaults.max_runs,
);
let expected_runs = 3 * REVIEW_CYCLES + 7;
assert_eq!(expected_runs, 31);
for run in 0..expected_runs {
itinerary
.record_run_started()
.unwrap_or_else(|error| panic!("run {run} must be permitted: {error}"));
itinerary.note_unreported_cost();
}
assert!(
itinerary.has_cost_reporting_gap(),
"the Tower must be able to surface silent metering"
);
assert!(
itinerary.runs_remaining() > 0,
"the cap must leave headroom rather than land exactly on the expected count"
);
}