use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use super::demand::{validate_id, Demand, DEMAND_SCHEMA_VERSION};
pub fn queue_dir(root: &Path) -> PathBuf {
root.join(".sdd").join("queue")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EnqueueOutcome {
Enqueued,
DuplicateIgnored,
}
fn demand_path(root: &Path, id: &str) -> Result<PathBuf> {
validate_id(id)?;
Ok(queue_dir(root).join(format!("{id}.json")))
}
pub fn enqueue(root: &Path, demand: &Demand) -> Result<EnqueueOutcome> {
if demand.schema_version != DEMAND_SCHEMA_VERSION {
bail!(
"schema_version de demanda não suportado ao enfileirar: {} (esperado {})",
demand.schema_version,
DEMAND_SCHEMA_VERSION
);
}
let path = demand_path(root, &demand.id)?;
if path.exists() {
return Ok(EnqueueOutcome::DuplicateIgnored);
}
let body = serde_json::to_string_pretty(demand).context("serializando demanda")?;
super::write_atomic(&path, body.as_bytes())?;
Ok(EnqueueOutcome::Enqueued)
}
pub fn load(root: &Path, id: &str) -> Result<Option<Demand>> {
let path = demand_path(root, id)?;
if !path.exists() {
return Ok(None);
}
let text =
fs::read_to_string(&path).with_context(|| format!("lendo demanda {}", path.display()))?;
Ok(Some(parse_demand(&text).with_context(|| {
format!("desserializando demanda {}", path.display())
})?))
}
pub fn list(root: &Path) -> Result<Vec<Demand>> {
let dir = queue_dir(root);
let mut out = Vec::new();
let Ok(entries) = fs::read_dir(&dir) else {
return Ok(out);
};
let mut paths: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|x| x.to_str()) == Some("json"))
.collect();
paths.sort();
for path in paths {
let text = fs::read_to_string(&path)
.with_context(|| format!("lendo demanda {}", path.display()))?;
out.push(
parse_demand(&text)
.with_context(|| format!("desserializando demanda {}", path.display()))?,
);
}
Ok(out)
}
fn parse_demand(text: &str) -> Result<Demand> {
let demand: Demand = serde_json::from_str(text)?;
if demand.schema_version != DEMAND_SCHEMA_VERSION {
bail!(
"schema_version de demanda não suportado: {} (esperado {})",
demand.schema_version,
DEMAND_SCHEMA_VERSION
);
}
Ok(demand)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::orchestrator::demand::{DemandSource, DemandType};
fn demand(id: &str) -> Demand {
Demand::new(
id,
DemandType::Story,
"Título da demanda",
"desc",
DemandSource::Cli,
None,
"2026-06-08T00:00:00Z",
)
.unwrap()
}
#[test]
fn enqueue_then_load_round_trips() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
assert_eq!(
enqueue(root, &demand("DEM-1")).unwrap(),
EnqueueOutcome::Enqueued
);
assert!(queue_dir(root).join("DEM-1.json").is_file());
let loaded = load(root, "DEM-1").unwrap().unwrap();
assert_eq!(loaded.id, "DEM-1");
assert_eq!(loaded.slug, "titulo-da-demanda");
}
#[test]
fn enqueue_is_idempotent_by_id() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
enqueue(root, &demand("DEM-1")).unwrap();
let again = enqueue(root, &demand("DEM-1")).unwrap();
assert_eq!(again, EnqueueOutcome::DuplicateIgnored);
assert_eq!(list(root).unwrap().len(), 1);
}
#[test]
fn list_returns_sorted_demands() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
enqueue(root, &demand("DEM-2")).unwrap();
enqueue(root, &demand("DEM-1")).unwrap();
let ids: Vec<String> = list(root).unwrap().into_iter().map(|d| d.id).collect();
assert_eq!(ids, vec!["DEM-1".to_string(), "DEM-2".to_string()]);
}
#[test]
fn load_missing_is_none() {
let dir = tempfile::tempdir().unwrap();
assert!(load(dir.path(), "NOPE").unwrap().is_none());
}
#[test]
fn list_empty_when_no_queue_dir() {
let dir = tempfile::tempdir().unwrap();
assert!(list(dir.path()).unwrap().is_empty());
}
#[test]
fn divergent_schema_version_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir_all(queue_dir(root)).unwrap();
let bad = r#"{ "schema_version": 99, "id": "DEM-9", "type": "task",
"title": "x", "description": "", "source": "cli", "priority": "normal",
"created_at": "2026-06-08T00:00:00Z", "slug": "x", "status": "queued" }"#;
fs::write(queue_dir(root).join("DEM-9.json"), bad).unwrap();
let err = load(root, "DEM-9").unwrap_err();
assert!(
err.to_string().contains("schema_version")
|| format!("{err:#}").contains("schema_version")
);
}
#[test]
fn enqueue_rejects_unsafe_id_via_validate() {
let dir = tempfile::tempdir().unwrap();
assert!(load(dir.path(), "../escape").is_err());
}
}