use super::{open_board, open_board_locked};
use crate::backlog::{BacklogItem, ItemId};
use crate::error::{Error, Result};
use crate::sprint::{Sprint, SprintCapacity, SprintId, SprintState};
use crate::storage::{Backend, BacklogItemRepository, SprintRepository};
use chrono::{DateTime, Utc};
use std::path::Path;
pub async fn create_sprint(
project_dir: &Path,
id: &SprintId,
title: &str,
goal: Option<String>,
period: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<Sprint> {
if let Some((start, end)) = period
&& start > end
{
return Err(Error::InvalidSprintPeriod {
start: start.date_naive(),
end: end.date_naive(),
});
}
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
match SprintRepository::load(&repo, id).await {
Ok(_) => return Err(Error::SprintExists(id.clone())),
Err(Error::SprintNotFound(_)) => {}
Err(e) => return Err(e),
}
let mut sprint = Sprint::new(id.clone(), title, Utc::now())?;
if let Some(goal) = goal {
sprint.goal = goal;
}
if let Some((start, end)) = period {
sprint.start = Some(start);
sprint.end = Some(end);
}
SprintRepository::save(&repo, &sprint).await?;
repo.commit(&format!("pinto: add {}", sprint.id)).await?;
Ok(sprint)
}
pub async fn edit_sprint(
project_dir: &Path,
id: &SprintId,
title: Option<String>,
goal: Option<String>,
period: Option<(DateTime<Utc>, DateTime<Utc>)>,
) -> Result<Sprint> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
let mut sprint = SprintRepository::load(&repo, id).await?;
sprint.update_details(title, goal, period, Utc::now())?;
SprintRepository::save(&repo, &sprint).await?;
repo.commit(&format!("pinto: update {}", sprint.id)).await?;
Ok(sprint)
}
pub async fn delete_sprint(project_dir: &Path, id: &SprintId) -> Result<()> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
SprintRepository::load(&repo, id).await?;
let now = Utc::now();
let assigned = BacklogItemRepository::list(&repo)
.await?
.into_iter()
.filter(|item| item.sprint.as_deref() == Some(id.as_str()))
.collect::<Vec<_>>();
for mut item in assigned {
item.sprint = None;
item.updated = now;
BacklogItemRepository::save(&repo, &item).await?;
}
SprintRepository::delete(&repo, id).await?;
repo.commit(&format!("pinto: delete {id}")).await?;
Ok(())
}
pub async fn start_sprint(project_dir: &Path, id: &SprintId) -> Result<Sprint> {
transition_sprint(project_dir, id, Sprint::start).await
}
pub async fn close_sprint(project_dir: &Path, id: &SprintId) -> Result<Sprint> {
transition_sprint(project_dir, id, Sprint::close).await
}
async fn transition_sprint(
project_dir: &Path,
id: &SprintId,
transition: impl FnOnce(&mut Sprint, chrono::DateTime<Utc>) -> Result<()>,
) -> Result<Sprint> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
let mut sprint = SprintRepository::load(&repo, id).await?;
transition(&mut sprint, Utc::now())?;
SprintRepository::save(&repo, &sprint).await?;
repo.commit(&format!("pinto: update {}", sprint.id)).await?;
Ok(sprint)
}
pub(crate) async fn validate_sprint_assignment(repo: &Backend, raw: &str) -> Result<SprintId> {
let id = SprintId::new(raw)?;
let sprint = SprintRepository::load(repo, &id).await?;
if sprint.state == SprintState::Closed {
return Err(Error::SprintClosed(id));
}
Ok(id)
}
pub async fn assign_sprint(
project_dir: &Path,
sprint_id: &SprintId,
item_id: &ItemId,
) -> Result<BacklogItem> {
assign_sprint_raw(project_dir, sprint_id.as_str(), item_id).await
}
pub async fn assign_sprint_raw(
project_dir: &Path,
raw_sprint_id: &str,
item_id: &ItemId,
) -> Result<BacklogItem> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
let sprint_id = validate_sprint_assignment(&repo, raw_sprint_id).await?;
let mut item = BacklogItemRepository::load(&repo, item_id).await?;
item.sprint = Some(sprint_id.to_string());
item.updated = Utc::now();
BacklogItemRepository::save(&repo, &item).await?;
repo.commit(&format!("pinto: update {}", item.id)).await?;
Ok(item)
}
pub async fn assign_sprint_by_status(
project_dir: &Path,
sprint_id: &SprintId,
status: &str,
limit: Option<usize>,
) -> Result<Vec<BacklogItem>> {
let (_board_dir, repo, config, _lock) = open_board_locked(project_dir).await?;
if !config.columns.iter().any(|column| column == status) {
return Err(Error::UnknownStatus(status.to_string()));
}
if limit == Some(0) {
return Err(Error::InvalidFilterOption(
"--limit must be at least 1".to_string(),
));
}
validate_sprint_assignment(&repo, sprint_id.as_str()).await?;
let mut candidates = BacklogItemRepository::list(&repo)
.await?
.into_iter()
.filter(|item| item.status.as_str() == status)
.filter(|item| item.sprint.as_deref() != Some(sprint_id.as_str()))
.collect::<Vec<_>>();
if let Some(limit) = limit {
candidates.truncate(limit);
}
if let Some(item) = candidates.iter().find(|item| item.sprint.is_some())
&& let Some(assigned_sprint) = item.sprint.as_deref()
{
return Err(Error::InvalidFilterOption(format!(
"{} is already assigned to sprint {}; remove it before bulk assignment",
item.id, assigned_sprint
)));
}
let original = candidates.clone();
let now = Utc::now();
let mut assigned = Vec::with_capacity(candidates.len());
for (index, mut item) in candidates.into_iter().enumerate() {
item.sprint = Some(sprint_id.to_string());
item.updated = now;
if let Err(error) = BacklogItemRepository::save(&repo, &item).await {
rollback_bulk_assignment(&repo, &original[..=index], &error).await?;
return Err(error);
}
assigned.push(item);
}
if !assigned.is_empty()
&& let Err(error) = repo
.commit(&format!(
"pinto: assign {} item(s) to {}",
assigned.len(),
sprint_id
))
.await
{
rollback_bulk_assignment(&repo, &original, &error).await?;
return Err(error);
}
Ok(assigned)
}
async fn rollback_bulk_assignment(
repo: &Backend,
original: &[BacklogItem],
operation_error: &Error,
) -> Result<()> {
for item in original.iter().rev() {
if let Err(rollback_error) = BacklogItemRepository::save(repo, item).await {
return Err(Error::InvalidFilterOption(format!(
"{operation_error}; failed to roll back bulk assignment: {rollback_error}"
)));
}
}
Ok(())
}
pub async fn unassign_sprint(
project_dir: &Path,
sprint_id: &SprintId,
item_id: &ItemId,
) -> Result<BacklogItem> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
let mut item = BacklogItemRepository::load(&repo, item_id).await?;
if item.sprint.as_deref() != Some(sprint_id.as_str()) {
return Err(Error::NotInSprint {
item: item_id.clone(),
sprint: sprint_id.clone(),
});
}
item.sprint = None;
item.updated = Utc::now();
BacklogItemRepository::save(&repo, &item).await?;
repo.commit(&format!("pinto: update {}", item.id)).await?;
Ok(item)
}
pub async fn list_sprints(project_dir: &Path) -> Result<Vec<Sprint>> {
let (_board_dir, repo, _config) = open_board(project_dir).await?;
SprintRepository::list(&repo).await
}
pub async fn set_sprint_capacity(
project_dir: &Path,
id: &SprintId,
daily_work_hours: f64,
holiday_days: u32,
deduction_factor: f64,
) -> Result<SprintCapacity> {
let (_board_dir, repo, _config, _lock) = open_board_locked(project_dir).await?;
let mut sprint = SprintRepository::load(&repo, id).await?;
sprint.set_capacity(daily_work_hours, holiday_days, deduction_factor)?;
sprint.updated = Utc::now();
let capacity = sprint
.capacity()
.ok_or_else(|| Error::SprintCapacityUnset(id.clone()))?;
SprintRepository::save(&repo, &sprint).await?;
repo.commit(&format!("pinto: update {}", sprint.id)).await?;
Ok(capacity)
}
pub async fn sprint_capacity(project_dir: &Path, id: &SprintId) -> Result<SprintCapacity> {
let (_board_dir, repo, _config) = open_board(project_dir).await?;
let sprint = SprintRepository::load(&repo, id).await?;
sprint
.capacity()
.ok_or_else(|| Error::SprintCapacityUnset(id.clone()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::service::test_support::init_temp;
use crate::service::{NewItem, add_item, move_item};
use crate::sprint::SprintState;
use crate::storage::{Backend, FileRepository};
fn sid(s: &str) -> SprintId {
SprintId::new(s).expect("valid sprint id")
}
#[tokio::test]
async fn sprint_assignment_validation_checks_grammar_and_existence() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
create_sprint(
dir.path(),
&sid("S-2"),
"Sprint 2",
Some("Ship the sprint".to_string()),
None,
)
.await
.unwrap();
start_sprint(dir.path(), &sid("S-2")).await.unwrap();
close_sprint(dir.path(), &sid("S-2")).await.unwrap();
let repo = Backend::File(FileRepository::new(dir.path().join(".pinto")));
assert_eq!(
validate_sprint_assignment(&repo, "S 1").await.unwrap_err(),
Error::InvalidSprintId("S 1".to_string())
);
assert_eq!(
validate_sprint_assignment(&repo, "S-9").await.unwrap_err(),
Error::SprintNotFound(sid("S-9"))
);
assert_eq!(
validate_sprint_assignment(&repo, "S-1")
.await
.expect("existing sprint validates"),
sid("S-1")
);
assert_eq!(
validate_sprint_assignment(&repo, "S-2").await.unwrap_err(),
Error::SprintClosed(sid("S-2"))
);
}
fn date(y: i32, m: u32, d: u32) -> DateTime<Utc> {
chrono::NaiveDate::from_ymd_opt(y, m, d)
.expect("valid date")
.and_hms_opt(0, 0, 0)
.expect("valid time")
.and_utc()
}
#[tokio::test]
async fn create_persists_planned_sprint() {
let dir = init_temp().await;
let sprint = create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.expect("create succeeds");
assert_eq!(sprint.title, "Sprint 1");
assert_eq!(sprint.state, SprintState::Planned);
let repo = FileRepository::new(dir.path().join(".pinto"));
let loaded = SprintRepository::load(&repo, &sid("S-1")).await.unwrap();
assert_eq!(loaded, sprint);
}
#[tokio::test]
async fn create_stores_goal_as_body() {
let dir = init_temp().await;
let sprint = create_sprint(
dir.path(),
&sid("S-1"),
"Sprint 1",
Some("Ship the MVP".to_string()),
None,
)
.await
.unwrap();
assert_eq!(sprint.goal, "Ship the MVP");
}
#[tokio::test]
async fn create_rejects_duplicate_id() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "First", None, None)
.await
.unwrap();
let err = create_sprint(dir.path(), &sid("S-1"), "Second", None, None)
.await
.unwrap_err();
assert_eq!(err, Error::SprintExists(sid("S-1")));
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
SprintRepository::load(&repo, &sid("S-1"))
.await
.unwrap()
.title,
"First"
);
}
#[tokio::test]
async fn create_rejects_empty_title() {
let dir = init_temp().await;
let err = create_sprint(dir.path(), &sid("S-1"), " ", None, None)
.await
.unwrap_err();
assert_eq!(err, Error::EmptySprintTitle);
}
#[tokio::test]
async fn create_stores_planned_period() {
let dir = init_temp().await;
let start = date(2026, 7, 6);
let end = date(2026, 7, 20);
let sprint = create_sprint(
dir.path(),
&sid("S-1"),
"Sprint 1",
None,
Some((start, end)),
)
.await
.unwrap();
assert_eq!(sprint.start, Some(start));
assert_eq!(sprint.end, Some(end));
let repo = FileRepository::new(dir.path().join(".pinto"));
let loaded = SprintRepository::load(&repo, &sid("S-1")).await.unwrap();
assert_eq!(loaded.start, Some(start));
assert_eq!(loaded.end, Some(end));
}
#[tokio::test]
async fn create_rejects_period_with_start_after_end() {
let dir = init_temp().await;
let start = date(2026, 7, 20);
let end = date(2026, 7, 6);
let err = create_sprint(
dir.path(),
&sid("S-1"),
"Sprint 1",
None,
Some((start, end)),
)
.await
.unwrap_err();
assert_eq!(
err,
Error::InvalidSprintPeriod {
start: start.date_naive(),
end: end.date_naive(),
}
);
}
#[tokio::test]
async fn create_on_uninitialized_dir_prompts_init() {
let dir = tempfile::TempDir::new().expect("temp dir");
let err = create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap_err();
assert!(matches!(err, Error::NotInitialized { .. }), "got {err:?}");
}
#[tokio::test]
async fn start_moves_planned_to_active_and_persists() {
let dir = init_temp().await;
create_sprint(
dir.path(),
&sid("S-1"),
"Sprint 1",
Some("Ship the sprint".to_string()),
None,
)
.await
.unwrap();
let started = start_sprint(dir.path(), &sid("S-1")).await.unwrap();
assert_eq!(started.state, SprintState::Active);
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
SprintRepository::load(&repo, &sid("S-1"))
.await
.unwrap()
.state,
SprintState::Active
);
}
#[tokio::test]
async fn close_moves_active_to_closed() {
let dir = init_temp().await;
create_sprint(
dir.path(),
&sid("S-1"),
"Sprint 1",
Some("Ship the sprint".to_string()),
None,
)
.await
.unwrap();
start_sprint(dir.path(), &sid("S-1")).await.unwrap();
let closed = close_sprint(dir.path(), &sid("S-1")).await.unwrap();
assert_eq!(closed.state, SprintState::Closed);
}
#[tokio::test]
async fn close_from_planned_is_rejected() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
let err = close_sprint(dir.path(), &sid("S-1")).await.unwrap_err();
assert!(
matches!(err, Error::InvalidSprintTransition { .. }),
"got {err:?}"
);
}
#[tokio::test]
async fn start_missing_sprint_returns_not_found() {
let dir = init_temp().await;
let err = start_sprint(dir.path(), &sid("S-9")).await.unwrap_err();
assert_eq!(err, Error::SprintNotFound(sid("S-9")));
}
#[tokio::test]
async fn assign_sets_item_sprint_and_persists() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
let item = add_item(dir.path(), "Task", NewItem::default())
.await
.unwrap();
let assigned = assign_sprint(dir.path(), &sid("S-1"), &item.id)
.await
.expect("assign succeeds");
assert_eq!(assigned.sprint.as_deref(), Some("S-1"));
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
BacklogItemRepository::load(&repo, &item.id)
.await
.unwrap()
.sprint
.as_deref(),
Some("S-1")
);
}
#[tokio::test]
async fn assign_to_missing_sprint_returns_not_found() {
let dir = init_temp().await;
let item = add_item(dir.path(), "Task", NewItem::default())
.await
.unwrap();
let err = assign_sprint(dir.path(), &sid("S-9"), &item.id)
.await
.unwrap_err();
assert_eq!(err, Error::SprintNotFound(sid("S-9")));
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
BacklogItemRepository::load(&repo, &item.id)
.await
.unwrap()
.sprint,
None
);
}
#[tokio::test]
async fn assign_missing_item_returns_not_found() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
let err = assign_sprint(dir.path(), &sid("S-1"), &ItemId::new("T", 99))
.await
.unwrap_err();
assert!(matches!(err, Error::NotFound(_)), "got {err:?}");
}
#[tokio::test]
async fn bulk_assignment_selects_matching_items_in_rank_order_up_to_limit() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
for title in ["First", "Second", "Third"] {
add_item(dir.path(), title, NewItem::default())
.await
.unwrap();
}
move_item(dir.path(), &ItemId::new("T", 3), "in-progress")
.await
.unwrap();
let assigned = assign_sprint_by_status(dir.path(), &sid("S-1"), "todo", Some(2))
.await
.expect("bulk assignment succeeds");
assert_eq!(
assigned
.iter()
.map(|item| item.id.to_string())
.collect::<Vec<_>>(),
["T-1", "T-2"]
);
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
BacklogItemRepository::load(&repo, &ItemId::new("T", 1))
.await
.unwrap()
.sprint
.as_deref(),
Some("S-1")
);
assert_eq!(
BacklogItemRepository::load(&repo, &ItemId::new("T", 2))
.await
.unwrap()
.sprint
.as_deref(),
Some("S-1")
);
assert_eq!(
BacklogItemRepository::load(&repo, &ItemId::new("T", 3))
.await
.unwrap()
.sprint,
None
);
}
#[tokio::test]
async fn bulk_assignment_without_limit_assigns_all_matching_items_and_skips_target_members() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
for title in ["First", "Second", "Third"] {
add_item(dir.path(), title, NewItem::default())
.await
.unwrap();
}
assign_sprint(dir.path(), &sid("S-1"), &ItemId::new("T", 1))
.await
.unwrap();
move_item(dir.path(), &ItemId::new("T", 3), "in-progress")
.await
.unwrap();
let assigned = assign_sprint_by_status(dir.path(), &sid("S-1"), "todo", None)
.await
.expect("bulk assignment succeeds");
assert_eq!(
assigned
.iter()
.map(|item| item.id.to_string())
.collect::<Vec<_>>(),
["T-2"]
);
}
#[tokio::test]
async fn bulk_assignment_limit_does_not_count_target_members() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
for title in ["Already assigned", "Next in rank"] {
add_item(dir.path(), title, NewItem::default())
.await
.unwrap();
}
assign_sprint(dir.path(), &sid("S-1"), &ItemId::new("T", 1))
.await
.unwrap();
let assigned = assign_sprint_by_status(dir.path(), &sid("S-1"), "todo", Some(1))
.await
.expect("bulk assignment succeeds");
assert_eq!(
assigned
.iter()
.map(|item| item.id.to_string())
.collect::<Vec<_>>(),
["T-2"]
);
}
#[tokio::test]
async fn bulk_assignment_rejects_other_sprint_without_partial_assignment() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
create_sprint(dir.path(), &sid("S-2"), "Sprint 2", None, None)
.await
.unwrap();
for title in ["Already assigned", "Still todo"] {
add_item(dir.path(), title, NewItem::default())
.await
.unwrap();
}
assign_sprint(dir.path(), &sid("S-2"), &ItemId::new("T", 1))
.await
.unwrap();
let err = assign_sprint_by_status(dir.path(), &sid("S-1"), "todo", None)
.await
.unwrap_err();
assert!(
matches!(&err, Error::InvalidFilterOption(message) if message.contains("T-1") && message.contains("S-2")),
"got {err:?}"
);
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
BacklogItemRepository::load(&repo, &ItemId::new("T", 2))
.await
.unwrap()
.sprint,
None
);
}
#[tokio::test]
async fn bulk_assignment_rejects_unknown_status_and_zero_limit_without_changes() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
let item = add_item(dir.path(), "Task", NewItem::default())
.await
.unwrap();
assert_eq!(
assign_sprint_by_status(dir.path(), &sid("S-1"), "missing", None)
.await
.unwrap_err(),
Error::UnknownStatus("missing".to_string())
);
assert!(matches!(
assign_sprint_by_status(dir.path(), &sid("S-1"), "todo", Some(0))
.await
.unwrap_err(),
Error::InvalidFilterOption(message) if message.contains("limit")
));
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
BacklogItemRepository::load(&repo, &item.id)
.await
.unwrap()
.sprint,
None
);
}
#[tokio::test]
async fn bulk_assignment_rejects_missing_or_closed_sprint_without_changes() {
let dir = init_temp().await;
let item = add_item(dir.path(), "Task", NewItem::default())
.await
.unwrap();
assert_eq!(
assign_sprint_by_status(dir.path(), &sid("S-9"), "todo", None)
.await
.unwrap_err(),
Error::SprintNotFound(sid("S-9"))
);
create_sprint(
dir.path(),
&sid("S-1"),
"Sprint 1",
Some("Ship it".to_string()),
None,
)
.await
.unwrap();
start_sprint(dir.path(), &sid("S-1")).await.unwrap();
close_sprint(dir.path(), &sid("S-1")).await.unwrap();
assert_eq!(
assign_sprint_by_status(dir.path(), &sid("S-1"), "todo", None)
.await
.unwrap_err(),
Error::SprintClosed(sid("S-1"))
);
let repo = FileRepository::new(dir.path().join(".pinto"));
assert_eq!(
BacklogItemRepository::load(&repo, &item.id)
.await
.unwrap()
.sprint,
None
);
}
#[tokio::test]
async fn unassign_clears_item_sprint() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
let item = add_item(dir.path(), "Task", NewItem::default())
.await
.unwrap();
assign_sprint(dir.path(), &sid("S-1"), &item.id)
.await
.unwrap();
let cleared = unassign_sprint(dir.path(), &sid("S-1"), &item.id)
.await
.expect("unassign succeeds");
assert_eq!(cleared.sprint, None);
}
#[tokio::test]
async fn unassign_item_not_in_sprint_returns_error() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "Sprint 1", None, None)
.await
.unwrap();
let item = add_item(dir.path(), "Task", NewItem::default())
.await
.unwrap();
let err = unassign_sprint(dir.path(), &sid("S-1"), &item.id)
.await
.unwrap_err();
assert_eq!(
err,
Error::NotInSprint {
item: item.id,
sprint: sid("S-1"),
}
);
}
#[tokio::test]
async fn list_returns_sprints_in_creation_order() {
let dir = init_temp().await;
create_sprint(dir.path(), &sid("S-1"), "First", None, None)
.await
.unwrap();
create_sprint(dir.path(), &sid("S-2"), "Second", None, None)
.await
.unwrap();
let ids: Vec<String> = list_sprints(dir.path())
.await
.expect("list succeeds")
.into_iter()
.map(|s| s.id.as_str().to_string())
.collect();
assert_eq!(ids, ["S-1", "S-2"]);
}
#[tokio::test]
async fn list_on_empty_board_is_empty() {
let dir = init_temp().await;
assert!(list_sprints(dir.path()).await.unwrap().is_empty());
}
}