Skip to main content

opencode_voice/approval/
queue.rs

1//! FIFO queue for pending approval items (permissions and questions).
2
3use std::collections::VecDeque;
4
5use crate::approval::types::{PendingApproval, PermissionRequest, QuestionRequest};
6
7/// FIFO queue for pending permission and question approvals.
8pub struct ApprovalQueue {
9    items: VecDeque<PendingApproval>,
10}
11
12impl ApprovalQueue {
13    pub fn new() -> Self {
14        ApprovalQueue {
15            items: VecDeque::new(),
16        }
17    }
18
19    /// Returns the number of pending items.
20    pub fn len(&self) -> usize {
21        self.items.len()
22    }
23
24    /// Returns true if there are pending items.
25    pub fn has_pending(&self) -> bool {
26        !self.items.is_empty()
27    }
28
29    /// Returns a reference to the front item without removing it.
30    pub fn peek(&self) -> Option<&PendingApproval> {
31        self.items.front()
32    }
33
34    /// Adds a permission request to the back of the queue.
35    pub fn add_permission(&mut self, request: PermissionRequest) {
36        self.items.push_back(PendingApproval::Permission(request));
37    }
38
39    /// Adds a question request to the back of the queue.
40    pub fn add_question(&mut self, request: QuestionRequest) {
41        self.items.push_back(PendingApproval::Question(request));
42    }
43
44    /// Removes the item with the given request ID. Returns true if found and removed.
45    pub fn remove(&mut self, request_id: &str) -> bool {
46        if let Some(pos) = self.items.iter().position(|item| item.id() == request_id) {
47            self.items.remove(pos);
48            true
49        } else {
50            false
51        }
52    }
53}
54
55impl Default for ApprovalQueue {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::approval::types::{PermissionRequest, QuestionRequest};
65
66    fn make_permission(id: &str) -> PermissionRequest {
67        PermissionRequest {
68            id: id.to_string(),
69            permission: "bash".to_string(),
70            metadata: serde_json::Value::Null,
71        }
72    }
73
74    fn make_question(id: &str) -> QuestionRequest {
75        QuestionRequest {
76            id: id.to_string(),
77            questions: vec![],
78        }
79    }
80
81    #[test]
82    fn test_new_queue_is_empty() {
83        let q = ApprovalQueue::new();
84        assert!(!q.has_pending());
85        assert_eq!(q.len(), 0);
86        assert!(q.peek().is_none());
87    }
88
89    #[test]
90    fn test_add_permission_and_peek() {
91        let mut q = ApprovalQueue::new();
92        q.add_permission(make_permission("perm-1"));
93        assert_eq!(q.len(), 1);
94        assert!(q.has_pending());
95        let item = q.peek().unwrap();
96        assert_eq!(item.id(), "perm-1");
97        assert!(matches!(item, PendingApproval::Permission(_)));
98    }
99
100    #[test]
101    fn test_add_question_and_peek() {
102        let mut q = ApprovalQueue::new();
103        q.add_question(make_question("q-1"));
104        assert_eq!(q.len(), 1);
105        let item = q.peek().unwrap();
106        assert_eq!(item.id(), "q-1");
107        assert!(matches!(item, PendingApproval::Question(_)));
108    }
109
110    #[test]
111    fn test_fifo_ordering() {
112        let mut q = ApprovalQueue::new();
113        q.add_permission(make_permission("first"));
114        q.add_question(make_question("second"));
115        q.add_permission(make_permission("third"));
116
117        // Peek should return first
118        assert_eq!(q.peek().unwrap().id(), "first");
119        assert_eq!(q.len(), 3);
120    }
121
122    #[test]
123    fn test_remove_found() {
124        let mut q = ApprovalQueue::new();
125        q.add_permission(make_permission("perm-1"));
126        q.add_question(make_question("q-1"));
127
128        let removed = q.remove("perm-1");
129        assert!(removed);
130        assert_eq!(q.len(), 1);
131        assert_eq!(q.peek().unwrap().id(), "q-1");
132    }
133
134    #[test]
135    fn test_remove_not_found() {
136        let mut q = ApprovalQueue::new();
137        q.add_permission(make_permission("perm-1"));
138
139        let removed = q.remove("nonexistent");
140        assert!(!removed);
141        assert_eq!(q.len(), 1);
142    }
143
144    #[test]
145    fn test_remove_middle_item() {
146        let mut q = ApprovalQueue::new();
147        q.add_permission(make_permission("a"));
148        q.add_permission(make_permission("b"));
149        q.add_permission(make_permission("c"));
150
151        q.remove("b");
152        assert_eq!(q.len(), 2);
153        assert_eq!(q.peek().unwrap().id(), "a");
154    }
155
156    #[test]
157    fn test_has_pending_tracks_state() {
158        let mut q = ApprovalQueue::new();
159        assert!(!q.has_pending());
160
161        q.add_permission(make_permission("x"));
162        assert!(q.has_pending());
163    }
164
165    // --- Additional tests added to expand coverage ---
166
167    #[test]
168    fn test_default_creates_empty_queue() {
169        let q = ApprovalQueue::default();
170        assert_eq!(q.len(), 0);
171    }
172
173    #[test]
174    fn test_remove_last_item_leaves_empty() {
175        let mut q = ApprovalQueue::new();
176        q.add_permission(make_permission("only"));
177        let removed = q.remove("only");
178        assert!(removed);
179        assert_eq!(q.len(), 0);
180        assert!(q.peek().is_none());
181    }
182
183    #[test]
184    fn test_remove_preserves_fifo_order() {
185        let mut q = ApprovalQueue::new();
186        q.add_permission(make_permission("a"));
187        q.add_question(make_question("b"));
188        q.add_permission(make_permission("c"));
189
190        // Remove middle item
191        q.remove("b");
192
193        // Remaining items should be in original order
194        assert_eq!(q.len(), 2);
195        assert_eq!(q.peek().unwrap().id(), "a");
196    }
197
198    #[test]
199    fn test_peek_does_not_remove() {
200        let mut q = ApprovalQueue::new();
201        q.add_permission(make_permission("p1"));
202        q.add_permission(make_permission("p2"));
203
204        // Peek multiple times — should always return same item
205        assert_eq!(q.peek().unwrap().id(), "p1");
206        assert_eq!(q.peek().unwrap().id(), "p1");
207        assert_eq!(q.len(), 2);
208    }
209
210    #[test]
211    fn test_insertion_order_preserved() {
212        let mut q = ApprovalQueue::new();
213        q.add_permission(make_permission("first"));
214        q.add_question(make_question("second"));
215        q.add_permission(make_permission("third"));
216
217        assert_eq!(q.len(), 3);
218        assert_eq!(q.peek().unwrap().id(), "first");
219    }
220
221    #[test]
222    fn test_len_increments_correctly() {
223        let mut q = ApprovalQueue::new();
224        assert_eq!(q.len(), 0);
225        q.add_permission(make_permission("a"));
226        assert_eq!(q.len(), 1);
227        q.add_question(make_question("b"));
228        assert_eq!(q.len(), 2);
229        q.add_permission(make_permission("c"));
230        assert_eq!(q.len(), 3);
231    }
232
233    #[test]
234    fn test_remove_not_found_does_not_change_len() {
235        let mut q = ApprovalQueue::new();
236        q.add_permission(make_permission("a"));
237        q.add_permission(make_permission("b"));
238
239        let removed = q.remove("nonexistent");
240        assert!(!removed);
241        assert_eq!(q.len(), 2);
242    }
243
244    #[test]
245    fn test_mixed_permission_and_question_types() {
246        let mut q = ApprovalQueue::new();
247        q.add_permission(make_permission("perm"));
248        q.add_question(make_question("quest"));
249
250        let first = q.peek().unwrap();
251        assert!(matches!(first, PendingApproval::Permission(_)));
252        assert_eq!(first.id(), "perm");
253
254        q.remove("perm");
255        let second = q.peek().unwrap();
256        assert!(matches!(second, PendingApproval::Question(_)));
257        assert_eq!(second.id(), "quest");
258    }
259}