1use std::path::{Path, PathBuf};
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17pub const QUEUE_SCHEMA: &str = "vela.queue.v0.1";
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct QueuedAction {
29 pub kind: String,
30 pub frontier: PathBuf,
31 #[serde(default)]
32 pub args: Value,
33 pub queued_at: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Queue {
38 #[serde(default = "default_schema")]
39 pub schema: String,
40 #[serde(default)]
41 pub actions: Vec<QueuedAction>,
42}
43
44fn default_schema() -> String {
45 QUEUE_SCHEMA.to_string()
46}
47
48impl Default for Queue {
49 fn default() -> Self {
50 Self {
51 schema: QUEUE_SCHEMA.to_string(),
52 actions: Vec::new(),
53 }
54 }
55}
56
57#[must_use]
60pub fn default_queue_path() -> PathBuf {
61 if let Ok(path) = std::env::var("VELA_QUEUE_FILE") {
62 return PathBuf::from(path);
63 }
64 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
65 PathBuf::from(home).join(".vela").join("queue.json")
66}
67
68pub fn load(path: &Path) -> Result<Queue, String> {
71 if !path.exists() {
72 return Ok(Queue::default());
73 }
74 let raw =
75 std::fs::read_to_string(path).map_err(|e| format!("read queue {}: {e}", path.display()))?;
76 let queue: Queue =
77 serde_json::from_str(&raw).map_err(|e| format!("parse queue {}: {e}", path.display()))?;
78 Ok(queue)
79}
80
81pub fn save(path: &Path, queue: &Queue) -> Result<(), String> {
83 if let Some(parent) = path.parent() {
84 std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
85 }
86 let raw = serde_json::to_string_pretty(queue).map_err(|e| format!("serialize queue: {e}"))?;
87 std::fs::write(path, raw).map_err(|e| format!("write queue {}: {e}", path.display()))?;
88 Ok(())
89}
90
91pub fn append(path: &Path, action: QueuedAction) -> Result<(), String> {
93 let mut queue = load(path)?;
94 queue.actions.push(action);
95 save(path, &queue)
96}
97
98pub fn clear(path: &Path) -> Result<usize, String> {
100 let queue = load(path)?;
101 let dropped = queue.actions.len();
102 save(path, &Queue::default())?;
103 Ok(dropped)
104}
105
106pub fn replace_actions(path: &Path, actions: Vec<QueuedAction>) -> Result<(), String> {
109 save(
110 path,
111 &Queue {
112 schema: QUEUE_SCHEMA.to_string(),
113 actions,
114 },
115 )
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use serde_json::json;
122 use tempfile::TempDir;
123
124 #[test]
125 fn empty_queue_when_file_absent() {
126 let tmp = TempDir::new().unwrap();
127 let path = tmp.path().join("queue.json");
128 let q = load(&path).unwrap();
129 assert_eq!(q.actions.len(), 0);
130 assert_eq!(q.schema, QUEUE_SCHEMA);
131 }
132
133 #[test]
134 fn append_persists_and_round_trips() {
135 let tmp = TempDir::new().unwrap();
136 let path = tmp.path().join("queue.json");
137 append(
138 &path,
139 QueuedAction {
140 kind: "accept_proposal".to_string(),
141 frontier: PathBuf::from("/tmp/x.json"),
142 args: json!({"proposal_id": "vpr_x", "reviewer_id": "r:test", "reason": "ok"}),
143 queued_at: "2026-04-25T00:00:00Z".to_string(),
144 },
145 )
146 .unwrap();
147 let q = load(&path).unwrap();
148 assert_eq!(q.actions.len(), 1);
149 assert_eq!(q.actions[0].kind, "accept_proposal");
150 }
151
152 #[test]
153 fn clear_drops_all_actions() {
154 let tmp = TempDir::new().unwrap();
155 let path = tmp.path().join("queue.json");
156 for i in 0..3 {
157 append(
158 &path,
159 QueuedAction {
160 kind: "propose_review".to_string(),
161 frontier: PathBuf::from("/tmp/x.json"),
162 args: json!({"i": i}),
163 queued_at: "2026-04-25T00:00:00Z".to_string(),
164 },
165 )
166 .unwrap();
167 }
168 let dropped = clear(&path).unwrap();
169 assert_eq!(dropped, 3);
170 let q = load(&path).unwrap();
171 assert_eq!(q.actions.len(), 0);
172 }
173}