tandem-server 0.7.2

HTTP server for Tandem engine APIs
// Copyright (c) 2026 Frumu LTD
// Licensed under the Business Source License 1.1

use super::*;

#[derive(Default)]
pub(in crate::http::global) struct LeaseWorktreeCleanupResult {
    pub(in crate::http::global) cleaned_paths: Vec<String>,
    pub(in crate::http::global) failures: Vec<Value>,
}

pub(in crate::http::global) async fn cleanup_managed_worktrees_for_lease(
    state: &AppState,
    lease_id: &str,
    caller_authority: Option<(&AuthorizedHostEffect, &HostEffectRequest)>,
) -> LeaseWorktreeCleanupResult {
    let records = state
        .managed_worktrees
        .read()
        .await
        .values()
        .filter(|row| {
            row.lease_id.as_deref() == Some(lease_id)
                && caller_authority.is_none_or(|(_, caller_effect)| {
                    row.tenant_context == caller_effect.resource.tenant_context
                })
        })
        .cloned()
        .collect::<Vec<_>>();
    let mut result = LeaseWorktreeCleanupResult::default();
    for record in records {
        if let Some((caller_grant, caller_effect)) = caller_authority {
            if let Err(error) = caller_grant.revalidate(state, caller_effect) {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": error.code(),
                    "authority": "caller",
                }));
                continue;
            }
        }
        let effect = HostEffectRequest::new(
            HostAction::WorktreeCleanup,
            CanonicalHostResource::new(
                "managed_worktree",
                record.key.clone(),
                record.tenant_context.clone(),
            ),
            json!({
                "repository_id": &record.repository_id,
                "repo_root": &record.repo_root,
                "path": &record.path,
                "branch": &record.branch,
                "lease_id": lease_id,
                "cleanup_branch": record.cleanup_branch,
                "reason": "lease_released_or_expired",
            }),
        );
        let grant = match crate::action_authorization::authorize_internal_host_effect(
            state,
            "http.global.cleanup_managed_worktrees_for_lease",
            &effect,
        )
        .await
        {
            Ok(grant) => grant,
            Err(error) => {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": error.code(),
                }));
                continue;
            }
        };
        if let Err(error) = grant.revalidate(state, &effect) {
            result.failures.push(json!({
                "worktree_id": record.key,
                "code": error.code(),
            }));
            continue;
        }
        if let Some((caller_grant, caller_effect)) = caller_authority {
            if let Err(error) = caller_grant.revalidate(state, caller_effect) {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": error.code(),
                    "authority": "caller",
                }));
                continue;
            }
        }
        if crate::runtime::worktrees::validate_managed_worktree_path(
            &record.repo_root,
            StdPath::new(&record.path),
            false,
        )
        .is_err()
        {
            result.failures.push(json!({
                "worktree_id": record.key,
                "code": "WORKTREE_PATH_CONTAINMENT_FAILED",
            }));
            continue;
        }
        // Creation takes the same write guard before adopting or creating a
        // checkout. Revalidate the exact snapshot while holding it and keep it
        // through every destructive Git operation so a replacement record and
        // checkout cannot be deleted by stale cleanup.
        let mut managed_worktrees = state.managed_worktrees.write().await;
        if managed_worktrees.get(&record.key) != Some(&record) {
            result.failures.push(json!({
                "worktree_id": record.key,
                "code": "WORKTREE_RECORD_CHANGED",
            }));
            continue;
        }
        if let Some((caller_grant, caller_effect)) = caller_authority {
            if let Err(error) = caller_grant.revalidate(state, caller_effect) {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": error.code(),
                    "authority": "caller",
                }));
                continue;
            }
        }
        if let Err(error) = grant.revalidate(state, &effect) {
            result.failures.push(json!({
                "worktree_id": record.key,
                "code": error.code(),
            }));
            continue;
        }
        let replacement_owns_path = replacement_owns_authorized_path(&managed_worktrees, &record);
        if replacement_owns_path {
            result.failures.push(json!({
                "worktree_id": record.key,
                "code": "WORKTREE_RECORD_REPLACED",
            }));
            continue;
        }
        let registered_branch = match parse_registered_worktree_entries(&record.repo_root).await {
            Ok(entries) => entries
                .into_iter()
                .find(|entry| entry.path == record.path)
                .and_then(|entry| entry.branch),
            Err(_) => {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": "WORKTREE_REGISTRATION_CHECK_FAILED",
                }));
                continue;
            }
        };
        let worktree_already_removed = registered_branch.is_none()
            && !StdPath::new(&record.path).exists()
            && record.cleanup_branch;
        if !worktree_already_removed && registered_branch.as_deref() != Some(record.branch.as_str())
        {
            result.failures.push(json!({
                "worktree_id": record.key,
                "expected_branch": record.branch,
                "registered_branch": registered_branch,
                "code": "WORKTREE_BRANCH_MISMATCH",
            }));
            continue;
        }
        if !worktree_already_removed {
            if let Some((caller_grant, caller_effect)) = caller_authority {
                if let Err(error) = caller_grant.revalidate(state, caller_effect) {
                    result.failures.push(json!({
                        "worktree_id": record.key,
                        "code": error.code(),
                        "authority": "caller",
                    }));
                    continue;
                }
            }
            if let Err(error) = grant.revalidate(state, &effect) {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": error.code(),
                }));
                continue;
            }
            let output = match crate::runtime::worktrees::run_managed_git(
                &record.repo_root,
                &["worktree", "remove", "--", &record.path],
            )
            .await
            {
                Ok(output) => output,
                Err(_) => {
                    result.failures.push(json!({
                        "path": record.path,
                        "branch": record.branch,
                        "repo_root": record.repo_root,
                        "code": "WORKTREE_REMOVE_FAILED",
                    }));
                    continue;
                }
            };
            if !output.success {
                result.failures.push(json!({
                    "path": record.path,
                    "branch": record.branch,
                    "repo_root": record.repo_root,
                    "code": "WORKTREE_REMOVE_FAILED",
                    "stderr": output.stderr.clone(),
                }));
                continue;
            }
        }
        let mut branch_cleanup_complete = true;
        if record.cleanup_branch {
            if let Some((caller_grant, caller_effect)) = caller_authority {
                if let Err(error) = caller_grant.revalidate(state, caller_effect) {
                    result.failures.push(json!({
                        "worktree_id": record.key,
                        "code": error.code(),
                        "authority": "caller",
                    }));
                    continue;
                }
            }
            if let Err(error) = grant.revalidate(state, &effect) {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": error.code(),
                }));
                continue;
            }
            match crate::runtime::worktrees::run_managed_git(
                &record.repo_root,
                &["branch", "-D", "--", &record.branch],
            )
            .await
            {
                Ok(branch_output) if branch_output.success => {}
                Ok(branch_output) => {
                    branch_cleanup_complete = false;
                    result.failures.push(json!({
                        "path": record.path,
                        "branch": record.branch,
                        "repo_root": record.repo_root,
                        "code": "WORKTREE_BRANCH_DELETE_FAILED",
                        "stderr": branch_output.stderr.clone(),
                    }));
                }
                Err(_) => {
                    branch_cleanup_complete = false;
                    result.failures.push(json!({
                        "path": record.path,
                        "branch": record.branch,
                        "repo_root": record.repo_root,
                        "code": "WORKTREE_BRANCH_DELETE_FAILED",
                    }));
                }
            }
        }
        if !branch_cleanup_complete {
            continue;
        }
        if let Some((caller_grant, caller_effect)) = caller_authority {
            if let Err(error) = caller_grant.revalidate(state, caller_effect) {
                result.failures.push(json!({
                    "worktree_id": record.key,
                    "code": error.code(),
                    "authority": "caller",
                }));
                continue;
            }
        }
        if let Err(error) = grant.revalidate(state, &effect) {
            result.failures.push(json!({
                "worktree_id": record.key,
                "code": error.code(),
            }));
            continue;
        }
        if !remove_authorized_cleanup_record(&mut managed_worktrees, &record) {
            result.failures.push(json!({
                "worktree_id": record.key,
                "code": "WORKTREE_RECORD_CHANGED",
            }));
            continue;
        }
        result.cleaned_paths.push(record.path);
    }
    result
}

fn remove_authorized_cleanup_record(
    records: &mut std::collections::HashMap<String, crate::ManagedWorktreeRecord>,
    authorized: &crate::ManagedWorktreeRecord,
) -> bool {
    if records.get(&authorized.key) != Some(authorized) {
        return false;
    }
    records.remove(&authorized.key);
    true
}

fn replacement_owns_authorized_path(
    records: &std::collections::HashMap<String, crate::ManagedWorktreeRecord>,
    authorized: &crate::ManagedWorktreeRecord,
) -> bool {
    records.values().any(|current| {
        current.key != authorized.key
            && current.repo_root == authorized.repo_root
            && current.path == authorized.path
    })
}

#[cfg(test)]
mod cleanup_record_tests {
    use super::*;

    fn record(key: &str, lease_id: &str) -> crate::ManagedWorktreeRecord {
        crate::ManagedWorktreeRecord {
            key: key.to_string(),
            repo_root: "/repo".to_string(),
            repository_id: Some("repo-1".to_string()),
            tenant_context: TenantContext::local_implicit(),
            path: "/repo/.tandem/worktrees/shared".to_string(),
            branch: format!("tandem/{key}"),
            base: "HEAD".to_string(),
            managed: true,
            task_id: Some("task".to_string()),
            owner_run_id: Some("run".to_string()),
            lease_id: Some(lease_id.to_string()),
            cleanup_branch: true,
            created_at_ms: 1,
            updated_at_ms: 1,
        }
    }

    #[test]
    fn cleanup_removes_only_the_exact_authorized_record() {
        let authorized = record("authorized", "lease-old");
        let replacement = record("replacement", "lease-new");
        let mut records = std::collections::HashMap::from([
            (authorized.key.clone(), authorized.clone()),
            (replacement.key.clone(), replacement.clone()),
        ]);

        assert!(replacement_owns_authorized_path(&records, &authorized));
        assert!(remove_authorized_cleanup_record(&mut records, &authorized));
        assert!(!records.contains_key(&authorized.key));
        assert_eq!(records.get(&replacement.key), Some(&replacement));
    }

    #[test]
    fn cleanup_refuses_a_changed_authorized_record() {
        let authorized = record("authorized", "lease-old");
        let mut changed = authorized.clone();
        changed.updated_at_ms = 2;
        let mut records = std::collections::HashMap::from([(changed.key.clone(), changed.clone())]);

        assert!(!remove_authorized_cleanup_record(&mut records, &authorized));
        assert_eq!(records.get(&changed.key), Some(&changed));
    }
}