use super::*;
use crate::routing::quota::live::CacheDirGuard;
use crate::support::paths::AidHomeGuard;
use crate::types::{Task, TaskStatus, VerifyStatus};
fn task() -> Task {
Task {
id: TaskId("t-billing".into()),
agent: AgentKind::Codex,
custom_agent_name: None,
prompt: "prompt".into(),
resolved_prompt: None,
category: None,
status: TaskStatus::Running,
parent_task_id: None,
workgroup_id: None,
caller_kind: None,
caller_session_id: None,
agent_session_id: None,
repo_path: None,
project_id: None,
worktree_path: None,
effective_dir: None,
worktree_branch: None,
final_head_sha: None,
final_branch: None,
start_sha: None,
log_path: None,
output_path: None,
tokens: None,
prompt_tokens: None,
duration_ms: None,
requested_model: None,
observed_model: None,
attribution_source: None,
cost_usd: None,
exit_code: None,
created_at: chrono::Local::now(),
completed_at: None,
verify: None,
verify_status: VerifyStatus::Skipped,
pending_reason: None,
read_only: false,
budget: false,
audit_verdict: None,
audit_report_path: None,
delivery_assessment: None,
}
}
#[test]
fn dispatch_records_overage_once_and_does_not_block_exhausted() {
let temp = tempfile::tempdir().expect("temp");
let _home = AidHomeGuard::set(temp.path());
let _cache = CacheDirGuard::set(temp.path());
let store = Store::open_memory().expect("store");
let task = task();
store.insert_task(&task).expect("task");
for (state, expected) in [("exhausted", 0), ("in_plan", 0), ("overage", 1), ("overage", 1)] {
std::fs::write(
temp.path().join("codex.json"),
serde_json::json!({
"ok":true,"snapshot":{"provider":"codex","windows":[],
"fetched_at":"2099-01-01T00:00:00Z","billing":{"state":state,
"credits":{"balance":12.25,"unlimited":false}}}
})
.to_string(),
)
.expect("cache");
record(&store, &task.id, task.agent).expect("explicit dispatch permitted");
let events = store.get_events(task.id.as_str()).expect("events");
assert_eq!(events.len(), expected);
if let Some(event) = events.first() {
assert_eq!(event.detail, "billing: overage (credits balance 12.25)");
assert_eq!(
event.metadata.as_ref().expect("metadata")["billing_state"],
"overage"
);
}
}
}
#[test]
fn billing_insert_failure_does_not_abort_dispatch_after_running_transition() {
let temp = tempfile::tempdir().expect("temp");
let _home = AidHomeGuard::set(temp.path());
let _cache = CacheDirGuard::set(temp.path());
let store = std::sync::Arc::new(Store::open_memory().expect("store"));
let mut task = task();
task.status = TaskStatus::Pending;
store.insert_task(&task).expect("task");
std::fs::write(temp.path().join("codex.json"), serde_json::json!({"ok":true,
"snapshot":{"provider":"codex","windows":[],"fetched_at":"2099-01-01T00:00:00Z",
"billing":{"state":"overage"}}}).to_string()).expect("cache");
store.db().execute_batch(concat!(
"CREATE TRIGGER reject_billing BEFORE INSERT ON events ",
"WHEN NEW.detail LIKE 'billing:%' BEGIN SELECT RAISE(FAIL, 'billing test failure'); END;"
)).expect("trigger");
assert!(record(&store, &task.id, task.agent).is_err(), "fault must be exercised");
let prepared = prepared(task, temp.path());
super::super::start_dispatch(&store, &prepared, &[]).expect("billing is advisory");
let saved = store.get_task(prepared.task_id.as_str()).expect("query").expect("task");
assert_eq!(saved.status, TaskStatus::Running);
assert!(store.get_events(prepared.task_id.as_str()).expect("events")
.iter().all(|event| !event.detail.starts_with("billing:")));
}
fn prepared(task: Task, dir: &std::path::Path) -> super::super::PreparedDispatch {
super::super::PreparedDispatch {
detected_project: None,
project_root: None,
agent_kind: task.agent,
agent_display_name: task.agent.as_str().into(),
requested_skills: Vec::new(),
effective_model: None,
budget_active: false,
agent: crate::agent::get_agent(task.agent),
task_id: task.id.clone(),
task,
log_path: dir.join("task.log"),
workgroup: None,
repo_path: None,
wt_path: None,
effective_dir: None,
}
}