use std::path::Path;
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,
origin: crate::opslog::Origin,
) -> Result<(), String> {
let Some(refusal) = ui.ceiling().refusal(workspace, &ui.prices()) else {
return Ok(());
};
let entry = OpEntry::step_failure(
ts.to_owned(),
STEP,
workspace.display().to_string(),
refusal.clone(),
origin,
);
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;
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}"#;
#[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(), Origin::Balls).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(), Origin::Balls).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(), Origin::Balls).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 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(), Origin::Balls).is_err());
}
}