use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const QUEUE_SCHEMA: &str = "vela.queue.v0.1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueuedAction {
pub kind: String,
pub frontier: PathBuf,
#[serde(default)]
pub args: Value,
pub queued_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Queue {
#[serde(default = "default_schema")]
pub schema: String,
#[serde(default)]
pub actions: Vec<QueuedAction>,
}
fn default_schema() -> String {
QUEUE_SCHEMA.to_string()
}
impl Default for Queue {
fn default() -> Self {
Self {
schema: QUEUE_SCHEMA.to_string(),
actions: Vec::new(),
}
}
}
#[must_use]
pub fn default_queue_path() -> PathBuf {
if let Ok(path) = std::env::var("VELA_QUEUE_FILE") {
return PathBuf::from(path);
}
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".vela").join("queue.json")
}
pub fn load(path: &Path) -> Result<Queue, String> {
if !path.exists() {
return Ok(Queue::default());
}
let raw =
std::fs::read_to_string(path).map_err(|e| format!("read queue {}: {e}", path.display()))?;
let queue: Queue =
serde_json::from_str(&raw).map_err(|e| format!("parse queue {}: {e}", path.display()))?;
Ok(queue)
}
pub fn save(path: &Path, queue: &Queue) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
}
let raw = serde_json::to_string_pretty(queue).map_err(|e| format!("serialize queue: {e}"))?;
std::fs::write(path, raw).map_err(|e| format!("write queue {}: {e}", path.display()))?;
Ok(())
}
pub fn append(path: &Path, action: QueuedAction) -> Result<(), String> {
let mut queue = load(path)?;
queue.actions.push(action);
save(path, &queue)
}
pub fn clear(path: &Path) -> Result<usize, String> {
let queue = load(path)?;
let dropped = queue.actions.len();
save(path, &Queue::default())?;
Ok(dropped)
}
pub fn replace_actions(path: &Path, actions: Vec<QueuedAction>) -> Result<(), String> {
save(
path,
&Queue {
schema: QUEUE_SCHEMA.to_string(),
actions,
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::TempDir;
#[test]
fn empty_queue_when_file_absent() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("queue.json");
let q = load(&path).unwrap();
assert_eq!(q.actions.len(), 0);
assert_eq!(q.schema, QUEUE_SCHEMA);
}
#[test]
fn append_persists_and_round_trips() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("queue.json");
append(
&path,
QueuedAction {
kind: "accept_proposal".to_string(),
frontier: PathBuf::from("/tmp/x.json"),
args: json!({"proposal_id": "vpr_x", "reviewer_id": "r:test", "reason": "ok"}),
queued_at: "2026-04-25T00:00:00Z".to_string(),
},
)
.unwrap();
let q = load(&path).unwrap();
assert_eq!(q.actions.len(), 1);
assert_eq!(q.actions[0].kind, "accept_proposal");
}
#[test]
fn clear_drops_all_actions() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("queue.json");
for i in 0..3 {
append(
&path,
QueuedAction {
kind: "propose_review".to_string(),
frontier: PathBuf::from("/tmp/x.json"),
args: json!({"i": i}),
queued_at: "2026-04-25T00:00:00Z".to_string(),
},
)
.unwrap();
}
let dropped = clear(&path).unwrap();
assert_eq!(dropped, 3);
let q = load(&path).unwrap();
assert_eq!(q.actions.len(), 0);
}
}