use std::collections::HashMap;
use std::sync::Mutex;
use crate::ports::TaskRepo;
use crate::types::*;
pub struct InMemoryTaskRepo {
tasks: Mutex<HashMap<TaskId, TaskSummary>>,
next_id: Mutex<i64>,
}
impl InMemoryTaskRepo {
pub fn new() -> Self {
Self {
tasks: Mutex::new(HashMap::new()),
next_id: Mutex::new(1),
}
}
}
impl TaskRepo for InMemoryTaskRepo {
fn create(&self, task: &Task<Conceptualizing>) -> TaskId {
let mut next = self.next_id.lock().unwrap();
let id = TaskId::parse(*next).unwrap();
*next += 1;
let mut summary = task.to_summary();
summary.id = id.clone();
self.tasks.lock().unwrap().insert(id.clone(), summary);
id
}
fn get(&self, id: &TaskId) -> Option<TaskSummary> {
self.tasks.lock().unwrap().get(id).cloned()
}
fn list_by_project(&self, project_id: &ProjectId) -> Vec<TaskSummary> {
self.tasks
.lock()
.unwrap()
.values()
.filter(|t| t.project_id == *project_id)
.cloned()
.collect()
}
fn save(&self, task: &TaskSummary) {
self.tasks
.lock()
.unwrap()
.insert(task.id.clone(), task.clone());
}
fn children(&self, parent_id: &TaskId) -> Vec<TaskSummary> {
self.tasks
.lock()
.unwrap()
.values()
.filter(|t| t.parent_id.as_ref() == Some(parent_id))
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mocks::fixtures::{pid, tname};
const ALPHA: &str = "alpha";
fn test_task() -> Task<Conceptualizing> {
Task::new(TaskData {
id: TaskId::parse(1).unwrap(),
project_id: pid(ALPHA),
parent_id: None,
name: tname("fix-auth"),
title: "Fix authentication".into(),
context_brief: None,
summary: None,
claude_session: None,
created_at: Utc::now(),
updated_at: Utc::now(),
})
}
#[test]
fn create_assigns_id() {
let repo = InMemoryTaskRepo::new();
let id = repo.create(&test_task());
assert_eq!(id.as_i64(), 1);
}
#[test]
fn create_increments_id() {
let repo = InMemoryTaskRepo::new();
let id1 = repo.create(&test_task());
let id2 = repo.create(&test_task());
assert_eq!(id1.as_i64(), 1);
assert_eq!(id2.as_i64(), 2);
}
#[test]
fn get_after_create() {
let repo = InMemoryTaskRepo::new();
let id = repo.create(&test_task());
let found = repo.get(&id).unwrap();
assert_eq!(found.name, tname("fix-auth"));
assert_eq!(found.status, TaskStatus::Conceptualizing);
}
#[test]
fn save_persists_state_change() {
let repo = InMemoryTaskRepo::new();
let id = repo.create(&test_task());
let summary = repo.get(&id).unwrap();
let typed = summary.into_typed();
match typed {
AnyTask::Conceptualizing(task) => {
let executing = task.execute();
repo.save(&executing.to_summary());
}
_ => panic!("expected Conceptualizing"),
}
let found = repo.get(&id).unwrap();
assert_eq!(found.status, TaskStatus::Executing);
}
#[test]
fn list_by_project_filters() {
let repo = InMemoryTaskRepo::new();
repo.create(&test_task());
repo.create(&Task::new(TaskData {
project_id: pid("other"),
..test_task().data
}));
let alpha_tasks = repo.list_by_project(&pid(ALPHA));
assert_eq!(alpha_tasks.len(), 1);
}
#[test]
fn children_finds_subtasks() {
let repo = InMemoryTaskRepo::new();
let parent_id = repo.create(&test_task());
repo.create(&Task::new(TaskData {
parent_id: Some(parent_id.clone()),
name: tname("subtask"),
..test_task().data
}));
let kids = repo.children(&parent_id);
assert_eq!(kids.len(), 1);
}
}