Skip to main content

layover_core/
queue.rs

1//! Work that has been asked for and not yet dispatched.
2//!
3//! # Why a queued flight is not just a flight
4//!
5//! A flight in motion needs nothing but itself. By the time it moves, the run it triggers has
6//! already had its prompt composed, and the pipeline's flags are baked into that text — so the
7//! flags have done their job and the flight can forget them.
8//!
9//! A *queued* flight has not reached that moment. Nothing has composed a prompt, because nothing
10//! has spawned a process. The pipeline it was triggered through and the flags the operator chose
11//! therefore have to survive in storage until something dispatches it, which may be after a
12//! restart and will certainly be after the request that set them has returned.
13//!
14//! Storing only the [`Flight`] loses both, silently: the operator sets `run_e2e`, the dialog
15//! reports success, and the run — whenever it happens — is composed as though they had not.
16
17use std::collections::BTreeMap;
18
19use serde::{Deserialize, Serialize};
20
21use crate::flight::Flight;
22use crate::pipeline::PipelineName;
23
24/// A flight waiting to be dispatched, with the instructions needed to dispatch it.
25#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
26pub struct Queued {
27    /// The flight itself.
28    pub flight: Flight,
29    /// The pipeline it was triggered through, when one was named rather than a bare agent.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub pipeline: Option<PipelineName>,
32    /// Every flag the pipeline declares, resolved to the value this run will see.
33    ///
34    /// Resolved, not merely the ones the operator changed: a default that shifts between queueing
35    /// and dispatch would otherwise change the meaning of work already booked.
36    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
37    pub flags: BTreeMap<String, bool>,
38}
39
40impl Queued {
41    /// Books `flight`, recording how it was asked for.
42    #[must_use]
43    pub fn new(
44        flight: Flight,
45        pipeline: Option<PipelineName>,
46        flags: BTreeMap<String, bool>,
47    ) -> Self {
48        Self {
49            flight,
50            pipeline,
51            flags,
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::agent::AgentName;
60    use crate::flight::{ItineraryId, Origin};
61
62    fn flight() -> Flight {
63        Flight::new(
64            ItineraryId::generate(),
65            Origin::Human,
66            AgentName::new("analyst"),
67            "look at 4821",
68            8,
69        )
70    }
71
72    #[test]
73    fn flags_survive_a_round_trip_through_storage() {
74        let mut flags = BTreeMap::new();
75        flags.insert("run_e2e".to_owned(), true);
76        flags.insert("draft_pr".to_owned(), false);
77
78        let queued = Queued::new(flight(), Some(PipelineName::new("development")), flags);
79
80        let json = serde_json::to_string(&queued).expect("serialises");
81        let read: Queued = serde_json::from_str(&json).expect("deserialises");
82
83        assert_eq!(read, queued);
84        assert_eq!(
85            read.flags.get("run_e2e"),
86            Some(&true),
87            "the operator set this and the run must see it"
88        );
89        assert_eq!(
90            read.flags.get("draft_pr"),
91            Some(&false),
92            "a flag left at its default is still a decision, and is recorded as one"
93        );
94    }
95
96    #[test]
97    fn a_bare_agent_trigger_carries_no_pipeline_and_no_flags() {
98        let queued = Queued::new(flight(), None, BTreeMap::new());
99        let json = serde_json::to_string(&queued).expect("serialises");
100
101        assert!(!json.contains("pipeline"), "{json}");
102        assert!(!json.contains("flags"), "{json}");
103    }
104}