trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! `delete_record` coverage — soft-delete `--deleted--` marker, the fail-closed
//! running-guard, `--force` bypass, and the workspace-dir-untouched invariant (#2012).
//!
//! Why: `session_manager/tests.rs` is at the 1500-SLOC test cap; this
//! #2012-specific coverage lives here so neither file grows past its limit,
//! mirroring the pattern established by `decommission_worktree_tests.rs` /
//! `backfill_tests.rs`. Reuses the sibling `tests` module's `make_manager` /
//! `seed_record` helpers rather than duplicating the scaffolding. The #2022
//! fix to the SAME guard (a real tmux liveness probe, not a persisted-state
//! check) is covered separately in `liveness_tests.rs` to keep this file
//! focused and both files under the SLOC cap.
//! What: four tests exercising [`super::manager::SessionManager::delete_record`]
//! — the `--deleted--` soft-delete mark, the running-guard refusal, the
//! `--force` bypass, and proof that the workspace directory is never touched.
//! Test: this file IS the test module; run with `cargo test -p trusty-mpm`.

use tempfile::TempDir;

use super::manager::ManagedError;
use super::record::{ManagedSessionId, ManagedSessionState};
use super::tests::{make_manager, seed_record};

/// `stop` REFUSES a terminal (Deleted/Decommissioned) record — the daemon-side
/// backstop against resurrection (code-critic CRITICAL).
///
/// Why: the zombie-reconcile resume path does `runtime-stop` then `resume`; if
/// `stop` flipped a `Deleted` tombstone to `Stopped`, the follow-up `resume`
/// would bring a DELETED session back to life. This pins that `stop` rejects a
/// terminal record with `InvalidState` and leaves its state unchanged.
/// Test: this function IS the test.
#[tokio::test]
async fn stop_refuses_terminal_record() {
    let dir = TempDir::new().unwrap();
    let (mgr, _fake) = make_manager(&dir).await;
    for terminal in [
        ManagedSessionState::Deleted,
        ManagedSessionState::Decommissioned,
    ] {
        let id = ManagedSessionId::new();
        seed_record(&mgr, &dir, id, terminal.clone(), false).await;

        let err = mgr
            .stop(&id)
            .await
            .expect_err("a terminal record must not be stoppable (resurrection guard)");
        assert!(
            matches!(err, ManagedError::InvalidState(_, _)),
            "expected InvalidState for {terminal:?}, got {err:?}"
        );
        // State is unchanged — the tombstone was not flipped to Stopped.
        assert_eq!(mgr.get(&id).await.expect("record present").state, terminal);
    }
}

/// `delete_record` marks a non-running record `--deleted--`, keeping it in the
/// store (#2012, soft-delete marker).
///
/// Why: `tm sessions delete` must REFLECT the deletion in the master list
/// (state `Deleted`, rendered `--deleted--`) rather than silently dropping the
/// record — the "fully-tracked lifecycle, no fire-and-forget" standard.
/// What: seeds a `Stopped` (non-running) record, deletes without `--force`, and
/// asserts it is STILL in the store with state `Deleted`.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_marks_deleted() {
    let dir = TempDir::new().unwrap();
    let (mgr, _fake) = make_manager(&dir).await;
    let id = ManagedSessionId::new();
    seed_record(&mgr, &dir, id, ManagedSessionState::Stopped, false).await;

    let prior = mgr.delete_record(&id, false).await.expect("delete");
    // The returned snapshot is the PRE-deletion state.
    assert_eq!(prior.state, ManagedSessionState::Stopped);
    // The record is still tracked, now marked `Deleted` (rendered `--deleted--`).
    let after = mgr.get(&id).await.expect("record must still exist");
    assert_eq!(after.state, ManagedSessionState::Deleted);
}

/// `delete_record` fail-closed guard refuses a RUNNING session without `--force` (#2012).
///
/// Why: the #2012 safety requirement — an operator must not be able to
/// accidentally hard-delete the record of a live session. The record must
/// still be present afterward (nothing mutated on refusal).
/// What: seeds an `Active` record, calls `delete_record(id, force=false)`,
/// asserts an `InvalidState` error naming the session, and asserts the record
/// is still retrievable (untouched).
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_refuses_running_without_force() {
    let dir = TempDir::new().unwrap();
    let (mgr, _fake) = make_manager(&dir).await;
    let id = ManagedSessionId::new();
    seed_record(&mgr, &dir, id, ManagedSessionState::Active, false).await;

    let err = mgr
        .delete_record(&id, false)
        .await
        .expect_err("must refuse to delete a running session without --force");
    assert!(
        matches!(err, ManagedError::InvalidState(ref sid, _) if sid == &id.to_string()),
        "expected InvalidState for {id}, got: {err:?}"
    );

    // The record must be untouched by the refused delete.
    let still_there = mgr.get(&id).await.expect("record must still exist");
    assert_eq!(still_there.state, ManagedSessionState::Active);
}

/// `--force` bypasses the running-state guard (#2012).
///
/// Why: an operator who explicitly opts in via `--force` must be able to
/// hard-delete a running session's record (e.g. to clear a stuck/duplicate
/// entry) without first stopping it.
/// What: seeds an `Active` record, calls `delete_record(id, force=true)`, and
/// asserts the record is marked `Deleted` (kept in the store).
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_force_bypasses_running_guard() {
    let dir = TempDir::new().unwrap();
    let (mgr, _fake) = make_manager(&dir).await;
    let id = ManagedSessionId::new();
    seed_record(&mgr, &dir, id, ManagedSessionState::Active, false).await;

    mgr.delete_record(&id, true)
        .await
        .expect("--force must bypass the running guard");
    let after = mgr.get(&id).await.expect("record must still exist");
    assert_eq!(after.state, ManagedSessionState::Deleted);
}

/// `delete_record` NEVER removes the workspace directory from disk (#2012).
///
/// Why: hard-deleting the RECORD is deliberately a store-only operation —
/// distinct from `decommission`, which may `remove_dir_all` an owned
/// workspace. A workspace dir that existed before delete must still exist
/// (untouched) after the record is gone.
/// What: seeds a `Stopped` record with a real on-disk workspace dir, deletes
/// the record, and asserts the workspace directory still exists on disk even
/// though the record is now marked `Deleted`.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_never_touches_workspace_dir() {
    let dir = TempDir::new().unwrap();
    let (mgr, _fake) = make_manager(&dir).await;
    let id = ManagedSessionId::new();
    let ws = seed_record(&mgr, &dir, id, ManagedSessionState::Stopped, false).await;
    assert!(ws.exists(), "test invariant: workspace dir must pre-exist");

    mgr.delete_record(&id, false).await.expect("delete");

    assert_eq!(
        mgr.get(&id).await.expect("record still tracked").state,
        ManagedSessionState::Deleted,
        "record must be marked Deleted (kept in the store)"
    );
    assert!(
        ws.exists(),
        "workspace directory must NOT be removed by delete_record (store-only op)"
    );
}

/// `delete_record` clears a stale `pending_decision`/`proposed_default` on the
/// persisted record (#4400).
///
/// Why: `Deleted` is terminal — the same reasoning `decommission_with_root`
/// applies (issue #4400): no human will ever act on a `pending_decision`
/// raised before a soft-delete, so leaving it set would let
/// `supervisor_status`'s human-confirmation queue accumulate another
/// phantom-gate source alongside decommissioned records.
/// What: seeds a `Stopped` record, sets a `pending_decision` +
/// `proposed_default` via `set_pending_decision`, deletes it, and asserts the
/// PERSISTED copy (`mgr.get`) has both fields cleared. The returned
/// PRE-deletion snapshot is expected to still carry the original decision
/// (documented behavior — callers render the state it was in before
/// deletion), so only the persisted copy is asserted clear.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_clears_pending_decision() {
    let dir = TempDir::new().unwrap();
    let (mgr, _fake) = make_manager(&dir).await;
    let id = ManagedSessionId::new();
    seed_record(&mgr, &dir, id, ManagedSessionState::Stopped, false).await;
    mgr.set_pending_decision(&id, "merge or rebase?", Some("merge"))
        .await
        .expect("set_pending_decision");

    mgr.delete_record(&id, false).await.expect("delete");

    let after = mgr.get(&id).await.expect("record must still exist");
    assert_eq!(after.state, ManagedSessionState::Deleted);
    assert!(
        after.pending_decision.is_none(),
        "pending_decision must be cleared by delete_record"
    );
    assert!(
        after.proposed_default.is_none(),
        "proposed_default must be cleared by delete_record"
    );
}