Support --before/--after positioning on task create
Allows inserting tasks at specific positions instead of always appending.
Extracted resolve_position helper shared by both create and move_task.
Introduced CreateTask struct to avoid clippy too-many-arguments warning.

Assisted-by: Claude Opus 4.6 via pi
change swzkpqvzlmzmmwtytsrqskmtpomwusqp
commit 4911803281221837222d8f69b19c2089ec001b64
author Alpha Chen <alpha@kejadlen.dev>
date
parent ykxwkvmz
diff --git a/src/bin/ranger/commands/task.rs b/src/bin/ranger/commands/task.rs
index 762acc0..d7fa8a6 100644
--- a/src/bin/ranger/commands/task.rs
+++ b/src/bin/ranger/commands/task.rs
@@ -26,6 +26,12 @@ pub enum TaskCommands {
         /// Tags to add (comma-separated)
         #[arg(long)]
         tag: Option<String>,
+        /// Place before this task key
+        #[arg(long)]
+        before: Option<String>,
+        /// Place after this task key
+        #[arg(long)]
+        after: Option<String>,
     },
     /// List tasks
     List {
@@ -99,6 +105,8 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> anyhow
             state,
             parent,
             tag,
+            before,
+            after,
         } => {
             let bl = ops::backlog::get_by_key_prefix(pool, &backlog).await?;
             let parent_id = if let Some(parent_key) = &parent {
@@ -107,6 +115,17 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> anyhow
                 None
             };
 
+            let before_id = if let Some(k) = &before {
+                Some(ops::task::get_by_key_prefix(pool, k).await?.id)
+            } else {
+                None
+            };
+            let after_id = if let Some(k) = &after {
+                Some(ops::task::get_by_key_prefix(pool, k).await?.id)
+            } else {
+                None
+            };
+
             let state = state
                 .map(|s| s.parse::<State>())
                 .transpose()
@@ -114,11 +133,15 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> anyhow
 
             let task = ops::task::create(
                 pool,
-                &title,
-                bl.id,
-                state,
-                parent_id,
-                description.as_deref(),
+                ops::task::CreateTask {
+                    title: &title,
+                    backlog_id: bl.id,
+                    state,
+                    parent_id,
+                    description: description.as_deref(),
+                    before_task_id: before_id,
+                    after_task_id: after_id,
+                },
             )
             .await?;
 
diff --git a/src/ops/blocker.rs b/src/ops/blocker.rs
index 093031f..83876cf 100644
--- a/src/ops/blocker.rs
+++ b/src/ops/blocker.rs
@@ -58,12 +58,34 @@ mod tests {
     async fn add_and_list_blockers() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = task::create(&pool, "Blocked", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = task::create(&pool, "Blocker", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = task::create(
+            &pool,
+            task::CreateTask {
+                title: "Blocked",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = task::create(
+            &pool,
+            task::CreateTask {
+                title: "Blocker",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         add(&pool, t1.id, t2.id).await.unwrap();
 
@@ -76,12 +98,34 @@ mod tests {
     async fn remove_blocker() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = task::create(&pool, "Blocked", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = task::create(&pool, "Blocker", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = task::create(
+            &pool,
+            task::CreateTask {
+                title: "Blocked",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = task::create(
+            &pool,
+            task::CreateTask {
+                title: "Blocker",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         add(&pool, t1.id, t2.id).await.unwrap();
         remove(&pool, t1.id, t2.id).await.unwrap();
diff --git a/src/ops/comment.rs b/src/ops/comment.rs
index 4463062..8196b74 100644
--- a/src/ops/comment.rs
+++ b/src/ops/comment.rs
@@ -41,9 +41,20 @@ mod tests {
     async fn add_and_list_comments() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t = task::create(&pool, "Task", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t = task::create(
+            &pool,
+            task::CreateTask {
+                title: "Task",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         add(&pool, t.id, "First comment").await.unwrap();
         add(&pool, t.id, "Second comment").await.unwrap();
diff --git a/src/ops/tag.rs b/src/ops/tag.rs
index fc0acd6..178228c 100644
--- a/src/ops/tag.rs
+++ b/src/ops/tag.rs
@@ -82,9 +82,20 @@ mod tests {
     async fn add_tag_to_task_and_list() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t = task::create(&pool, "Task", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t = task::create(
+            &pool,
+            task::CreateTask {
+                title: "Task",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
         let tag = get_or_create(&pool, "important").await.unwrap();
 
         add_to_task(&pool, t.id, tag.id).await.unwrap();
@@ -98,9 +109,20 @@ mod tests {
     async fn add_tag_to_task_is_idempotent() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t = task::create(&pool, "Task", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t = task::create(
+            &pool,
+            task::CreateTask {
+                title: "Task",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
         let tag = get_or_create(&pool, "dup").await.unwrap();
 
         add_to_task(&pool, t.id, tag.id).await.unwrap();
diff --git a/src/ops/task.rs b/src/ops/task.rs
index cd7c882..3a58069 100644
--- a/src/ops/task.rs
+++ b/src/ops/task.rs
@@ -4,16 +4,19 @@ use crate::models::{State, Task};
 use crate::position;
 use sqlx::SqlitePool;
 
-pub async fn create(
-    pool: &SqlitePool,
-    title: &str,
-    backlog_id: i64,
-    state: Option<State>,
-    parent_id: Option<i64>,
-    description: Option<&str>,
-) -> Result<Task, RangerError> {
+pub struct CreateTask<'a> {
+    pub title: &'a str,
+    pub backlog_id: i64,
+    pub state: Option<State>,
+    pub parent_id: Option<i64>,
+    pub description: Option<&'a str>,
+    pub before_task_id: Option<i64>,
+    pub after_task_id: Option<i64>,
+}
+
+pub async fn create(pool: &SqlitePool, params: CreateTask<'_>) -> Result<Task, RangerError> {
     let key = key::generate_key();
-    let state = state.unwrap_or(State::Icebox);
+    let state = params.state.unwrap_or(State::Icebox);
 
     let task = sqlx::query_as::<_, Task>(
         "INSERT INTO tasks (key, parent_id, title, description, state) \
@@ -21,29 +24,24 @@ pub async fn create(
          RETURNING id, key, parent_id, title, description, state, created_at, updated_at",
     )
     .bind(&key)
-    .bind(parent_id)
-    .bind(title)
-    .bind(description)
+    .bind(params.parent_id)
+    .bind(params.title)
+    .bind(params.description)
     .bind(state.as_str())
     .fetch_one(pool)
     .await?;
 
-    // Get the last position in this backlog+state to append
-    let last_pos: Option<String> = sqlx::query_scalar(
-        "SELECT bt.position FROM backlog_tasks bt \
-         JOIN tasks t ON t.id = bt.task_id \
-         WHERE bt.backlog_id = ? AND t.state = ? \
-         ORDER BY bt.position DESC LIMIT 1",
+    let new_pos = resolve_position(
+        pool,
+        params.backlog_id,
+        task.id,
+        params.before_task_id,
+        params.after_task_id,
     )
-    .bind(backlog_id)
-    .bind(state.as_str())
-    .fetch_optional(pool)
     .await?;
 
-    let new_pos = position::midpoint(last_pos.as_deref(), None);
-
     sqlx::query("INSERT INTO backlog_tasks (backlog_id, task_id, position) VALUES (?, ?, ?)")
-        .bind(backlog_id)
+        .bind(params.backlog_id)
         .bind(task.id)
         .bind(&new_pos)
         .execute(pool)
@@ -153,13 +151,33 @@ pub async fn edit(
     Ok(task)
 }
 
-pub async fn move_task(
+/// Compute a position string for a task within a backlog.
+///
+/// When both bounds are None, appends after the last task in the backlog.
+/// When `before_task_id` or `after_task_id` is given, positions relative
+/// to those tasks. `task_id` is excluded from adjacent-position lookups
+/// (relevant for moves where the task already has a position).
+async fn resolve_position(
     pool: &SqlitePool,
-    task_id: i64,
     backlog_id: i64,
+    task_id: i64,
     before_task_id: Option<i64>,
     after_task_id: Option<i64>,
-) -> Result<(), RangerError> {
+) -> Result<String, RangerError> {
+    if before_task_id.is_none() && after_task_id.is_none() {
+        // Append to end of backlog
+        let last_pos: Option<String> = sqlx::query_scalar(
+            "SELECT bt.position FROM backlog_tasks bt \
+             WHERE bt.backlog_id = ? \
+             ORDER BY bt.position DESC LIMIT 1",
+        )
+        .bind(backlog_id)
+        .fetch_optional(pool)
+        .await?;
+
+        return Ok(position::midpoint(last_pos.as_deref(), None));
+    }
+
     // "before" task = the task we want to appear after us (upper bound)
     // "after" task = the task we want to appear before us (lower bound)
     let upper_bound: Option<String> = if let Some(id) = before_task_id {
@@ -220,7 +238,18 @@ pub async fn move_task(
         _ => (lower_bound.clone(), upper_bound.clone()),
     };
 
-    let new_pos = position::midpoint(lower.as_deref(), upper.as_deref());
+    Ok(position::midpoint(lower.as_deref(), upper.as_deref()))
+}
+
+pub async fn move_task(
+    pool: &SqlitePool,
+    task_id: i64,
+    backlog_id: i64,
+    before_task_id: Option<i64>,
+    after_task_id: Option<i64>,
+) -> Result<(), RangerError> {
+    let new_pos =
+        resolve_position(pool, backlog_id, task_id, before_task_id, after_task_id).await?;
 
     sqlx::query("UPDATE backlog_tasks SET position = ? WHERE backlog_id = ? AND task_id = ?")
         .bind(&new_pos)
@@ -305,9 +334,20 @@ mod tests {
     async fn create_task_in_backlog() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let task = create(&pool, "My Task", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let task = create(
+            &pool,
+            CreateTask {
+                title: "My Task",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         assert_eq!(task.title, "My Task");
         assert_eq!(task.state, State::Icebox);
@@ -329,15 +369,48 @@ mod tests {
     async fn list_tasks_ordered_by_position() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = create(&pool, "First", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = create(&pool, "Second", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t3 = create(&pool, "Third", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Third",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         let tasks = list(&pool, bl.id, None).await.unwrap();
         assert_eq!(tasks.len(), 3);
@@ -350,12 +423,34 @@ mod tests {
     async fn list_tasks_with_state_filter() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        create(&pool, "Icebox task", bl.id, None, None, None)
-            .await
-            .unwrap();
-        create(&pool, "Queued task", bl.id, Some(State::Queued), None, None)
-            .await
-            .unwrap();
+        create(
+            &pool,
+            CreateTask {
+                title: "Icebox task",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        create(
+            &pool,
+            CreateTask {
+                title: "Queued task",
+                backlog_id: bl.id,
+                state: Some(State::Queued),
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         let icebox = list(&pool, bl.id, Some(State::Icebox)).await.unwrap();
         assert_eq!(icebox.len(), 1);
@@ -370,9 +465,20 @@ mod tests {
     async fn get_task_by_key_prefix() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let task = create(&pool, "Find me", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let task = create(
+            &pool,
+            CreateTask {
+                title: "Find me",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         let found = get_by_key_prefix(&pool, &task.key[..3]).await.unwrap();
         assert_eq!(found.id, task.id);
@@ -382,9 +488,20 @@ mod tests {
     async fn edit_task_fields() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let task = create(&pool, "Original", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let task = create(
+            &pool,
+            CreateTask {
+                title: "Original",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         let updated = edit(
             &pool,
@@ -406,9 +523,20 @@ mod tests {
         let pool = test_pool().await;
         let bl1 = backlog::create(&pool, "First").await.unwrap();
         let bl2 = backlog::create(&pool, "Second").await.unwrap();
-        let task = create(&pool, "Shared", bl1.id, None, None, None)
-            .await
-            .unwrap();
+        let task = create(
+            &pool,
+            CreateTask {
+                title: "Shared",
+                backlog_id: bl1.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         add_to_backlog(&pool, task.id, bl2.id).await.unwrap();
 
@@ -423,9 +551,20 @@ mod tests {
     async fn remove_task_from_backlog() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let task = create(&pool, "Remove me", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let task = create(
+            &pool,
+            CreateTask {
+                title: "Remove me",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         remove_from_backlog(&pool, task.id, bl.id).await.unwrap();
 
@@ -437,15 +576,48 @@ mod tests {
     async fn move_task_before() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = create(&pool, "First", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = create(&pool, "Second", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t3 = create(&pool, "Third", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Third",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         // Move t3 before t1 — should produce order: t3, t1, t2
         move_task(&pool, t3.id, bl.id, Some(t1.id), None)
@@ -462,15 +634,48 @@ mod tests {
     async fn move_task_after() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = create(&pool, "First", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = create(&pool, "Second", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t3 = create(&pool, "Third", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Third",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         // Move t1 after t3 — should produce order: t2, t3, t1
         move_task(&pool, t1.id, bl.id, None, Some(t3.id))
@@ -487,15 +692,48 @@ mod tests {
     async fn move_task_after_into_middle() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = create(&pool, "First", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = create(&pool, "Second", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t3 = create(&pool, "Third", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Third",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         // Move t3 after t1 (but before t2) — should produce order: t1, t3, t2
         move_task(&pool, t3.id, bl.id, None, Some(t1.id))
@@ -512,15 +750,48 @@ mod tests {
     async fn move_task_before_from_middle() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = create(&pool, "First", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = create(&pool, "Second", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t3 = create(&pool, "Third", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Third",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         // Move t1 before t3 (but after t2) — should produce order: t2, t1, t3
         move_task(&pool, t1.id, bl.id, Some(t3.id), None)
@@ -537,15 +808,48 @@ mod tests {
     async fn move_task_between() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let t1 = create(&pool, "First", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t2 = create(&pool, "Second", bl.id, None, None, None)
-            .await
-            .unwrap();
-        let t3 = create(&pool, "Third", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Third",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         // Move t3 after t1 and before t2 — should produce order: t1, t3, t2
         move_task(&pool, t3.id, bl.id, Some(t2.id), Some(t1.id))
@@ -562,9 +866,20 @@ mod tests {
     async fn delete_task() {
         let pool = test_pool().await;
         let bl = backlog::create(&pool, "Test").await.unwrap();
-        let task = create(&pool, "Delete me", bl.id, None, None, None)
-            .await
-            .unwrap();
+        let task = create(
+            &pool,
+            CreateTask {
+                title: "Delete me",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
 
         delete(&pool, task.id).await.unwrap();
 
@@ -575,4 +890,169 @@ mod tests {
         let tasks = list(&pool, bl.id, None).await.unwrap();
         assert_eq!(tasks.len(), 0);
     }
+
+    #[tokio::test]
+    async fn create_task_before() {
+        let pool = test_pool().await;
+        let bl = backlog::create(&pool, "Test").await.unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+
+        // Create a task before t1 — should produce order: t3, t1, t2
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Before first",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: Some(t1.id),
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+
+        let tasks = list(&pool, bl.id, None).await.unwrap();
+        assert_eq!(tasks[0].id, t3.id, "t3 should be first");
+        assert_eq!(tasks[1].id, t1.id, "t1 should be second");
+        assert_eq!(tasks[2].id, t2.id, "t2 should be third");
+    }
+
+    #[tokio::test]
+    async fn create_task_after() {
+        let pool = test_pool().await;
+        let bl = backlog::create(&pool, "Test").await.unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+
+        // Create a task after t1 — should produce order: t1, t3, t2
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "After first",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: Some(t1.id),
+            },
+        )
+        .await
+        .unwrap();
+
+        let tasks = list(&pool, bl.id, None).await.unwrap();
+        assert_eq!(tasks[0].id, t1.id, "t1 should be first");
+        assert_eq!(tasks[1].id, t3.id, "t3 should be second");
+        assert_eq!(tasks[2].id, t2.id, "t2 should be third");
+    }
+
+    #[tokio::test]
+    async fn create_task_between() {
+        let pool = test_pool().await;
+        let bl = backlog::create(&pool, "Test").await.unwrap();
+        let t1 = create(
+            &pool,
+            CreateTask {
+                title: "First",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+        let t2 = create(
+            &pool,
+            CreateTask {
+                title: "Second",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: None,
+                after_task_id: None,
+            },
+        )
+        .await
+        .unwrap();
+
+        // Create a task after t1 and before t2 — should produce order: t1, t3, t2
+        let t3 = create(
+            &pool,
+            CreateTask {
+                title: "Between",
+                backlog_id: bl.id,
+                state: None,
+                parent_id: None,
+                description: None,
+                before_task_id: Some(t2.id),
+                after_task_id: Some(t1.id),
+            },
+        )
+        .await
+        .unwrap();
+
+        let tasks = list(&pool, bl.id, None).await.unwrap();
+        assert_eq!(tasks[0].id, t1.id, "t1 should be first");
+        assert_eq!(tasks[1].id, t3.id, "t3 should be second");
+        assert_eq!(tasks[2].id, t2.id, "t2 should be third");
+    }
 }