use std::path::{Path, PathBuf};
use crate::opslog::{self, OpEntry};
use crate::ui_state::UiState;
const STEP: &str = "ceiling";
pub fn gate(
ui: &UiState,
state_root: &Path,
ts: &str,
workspace: &Path,
world: &[PathBuf],
origin: crate::opslog::Origin,
client: crate::registry::Client,
) -> Result<(), String> {
let Some(refusal) = ui.ceiling().refusal(world, &ui.prices()) else {
return Ok(());
};
let entry = OpEntry::step_failure(
ts.to_owned(),
STEP,
workspace.display().to_string(),
refusal.clone(),
origin,
client,
);
let _ = opslog::append(state_root, &entry);
Err(refusal)
}
#[cfg(test)]
mod tests {
use super::gate;
use crate::opslog::Origin;
use crate::ui_state::UiState;
use std::path::{Path, PathBuf};
const CONV: &str = "20260803T120000Z-root";
fn ui(dir: &Path, doc: &str) -> UiState {
let path = dir.join("ui.json");
std::fs::write(&path, doc).unwrap();
UiState::open(path)
}
fn spent(ws: &Path) {
let step = ws.join("steps").join(CONV).join("001");
std::fs::create_dir_all(&step).unwrap();
std::fs::write(
step.join("response.json"),
r#"{"type":"usage","input_tokens":3000000}"#,
)
.unwrap();
std::fs::write(step.join("request.json"), r#"{"model":"opus"}"#).unwrap();
}
const PRICED: &str = r#"{"v":1,"prices":{"opus":{"input":1}},"ceiling":2}"#;
fn world(dir: &Path) -> Vec<PathBuf> {
vec![dir.to_path_buf()]
}
#[test]
fn an_unconfigured_world_is_ungated() {
let dir = tempfile::tempdir().unwrap();
spent(dir.path());
let ui = ui(dir.path(), r#"{"v":1,"prices":{"opus":{"input":1}}}"#);
assert!(
gate(
&ui,
dir.path(),
"T1",
dir.path(),
&world(dir.path()),
Origin::Balls,
crate::registry::Client::default(),
)
.is_ok()
);
assert!(!dir.path().join("ops.jsonl").exists(), "nothing to log");
}
#[test]
fn under_the_ceiling_flies_and_logs_nothing() {
let dir = tempfile::tempdir().unwrap();
spent(dir.path());
let ui = ui(
dir.path(),
r#"{"v":1,"prices":{"opus":{"input":1}},"ceiling":5}"#,
);
assert!(
gate(
&ui,
dir.path(),
"T1",
dir.path(),
&world(dir.path()),
Origin::Balls,
crate::registry::Client::default(),
)
.is_ok()
);
assert!(!dir.path().join("ops.jsonl").exists());
}
#[test]
fn over_the_ceiling_refuses_and_leaves_the_ops_row() {
let dir = tempfile::tempdir().unwrap();
spent(dir.path());
let ui = ui(dir.path(), PRICED);
let refusal = gate(
&ui,
dir.path(),
"T1",
dir.path(),
&world(dir.path()),
Origin::Balls,
crate::registry::Client::default(),
)
.unwrap_err();
assert!(refusal.contains("spend ceiling reached"), "{refusal}");
let trail = std::fs::read_to_string(dir.path().join("ops.jsonl")).unwrap();
assert!(trail.contains("yog-step"), "{trail}");
assert!(trail.contains("ceiling"), "{trail}");
assert!(trail.contains("\"exit\":-3"), "{trail}");
}
#[test]
fn a_sibling_s_spend_refuses_an_idle_workspace_and_the_row_names_the_target() {
let dir = tempfile::tempdir().unwrap();
let (idle, busy) = (dir.path().join("idle"), dir.path().join("busy"));
std::fs::create_dir_all(&idle).unwrap();
spent(&busy);
let ui = ui(dir.path(), PRICED);
let roster = vec![idle.clone(), busy];
let refusal = gate(
&ui,
dir.path(),
"T1",
&idle,
&roster,
Origin::Balls,
crate::registry::Client::default(),
)
.unwrap_err();
assert!(refusal.contains("$3.00"), "{refusal}");
let trail = std::fs::read_to_string(dir.path().join("ops.jsonl")).unwrap();
assert!(trail.contains("idle"), "{trail}");
}
#[test]
fn an_unwritable_trail_still_refuses() {
let dir = tempfile::tempdir().unwrap();
spent(dir.path());
let ui = ui(dir.path(), PRICED);
let wall = dir.path().join("ui.json");
assert!(
gate(
&ui,
&wall,
"T1",
dir.path(),
&world(dir.path()),
Origin::Balls,
crate::registry::Client::default(),
)
.is_err()
);
}
}