@unit @coverage
Feature: CleanupService uncovered lines coverage
  As a developer maintaining the CleanupService
  I want complete test coverage for all edge-case and error-handling paths
  So that garbage-collection behaves correctly under adverse conditions

  # ── _get_sandbox_dirs edge cases ────────────────────────────────

  Scenario: _get_sandbox_dirs returns empty when tmp does not exist
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage calls _get_sandbox_dirs with tmp not existing
    Then cleanup coverage _get_sandbox_dirs result should be empty

  Scenario: _get_sandbox_dirs returns empty when iterdir raises OSError
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage calls _get_sandbox_dirs with iterdir raising OSError
    Then cleanup coverage _get_sandbox_dirs result should be empty

  Scenario: _get_sandbox_dirs skips entry when is_dir raises OSError
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage calls _get_sandbox_dirs with is_dir raising OSError on one entry
    Then cleanup coverage _get_sandbox_dirs result should contain only the safe entry

  # ── extract_plan_id_from_sandbox ────────────────────────────────

  Scenario: extract_plan_id_from_sandbox returns None for non-matching name
    When cleanup coverage extracts plan id from path "random-dir-name"
    Then cleanup coverage extracted plan id should be None

  # ── _is_sandbox_stale ──────────────────────────────────────────

  Scenario: _is_sandbox_stale returns False when stat raises OSError
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage checks staleness of a path that raises OSError on stat
    Then cleanup coverage staleness result should be False

  # ── _purge_sandboxes ───────────────────────────────────────────

  Scenario: _purge_sandboxes increments skipped when rmtree fails
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage purges sandboxes and rmtree raises OSError
    Then cleanup coverage purge sandbox report should show skipped count of 1

  # ── scan_checkpoints_for_plan ──────────────────────────────────

  Scenario: scan_checkpoints_for_plan returns empty when dir does not exist
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage scans checkpoints for a non-existent directory
    Then cleanup coverage checkpoint scan result should be empty

  Scenario: scan_checkpoints_for_plan returns empty when fewer than 3 files
    Given cleanup coverage has a CleanupService with max 1 checkpoint override
    When cleanup coverage scans checkpoints for a directory with 2 files
    Then cleanup coverage checkpoint scan result should be empty

  # ── prune_checkpoints_for_plan ─────────────────────────────────

  Scenario: prune_checkpoints_for_plan handles unlink OSError gracefully
    Given cleanup coverage has a CleanupService with max 2 checkpoints
    When cleanup coverage prunes checkpoints and unlink raises OSError
    Then cleanup coverage prune result should be 0

  # ── scan_inactive_sessions ─────────────────────────────────────

  Scenario: scan_inactive_sessions skips session when updated_at is None
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage scans sessions with a None updated_at entry
    Then cleanup coverage inactive sessions result should be empty

  Scenario: scan_inactive_sessions parses string updated_at
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage scans sessions with a string updated_at that is old
    Then cleanup coverage inactive sessions result should contain that session

  Scenario: scan_inactive_sessions adds UTC when tzinfo is missing
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage scans sessions with a naive datetime updated_at that is old
    Then cleanup coverage inactive sessions result should contain that session

  # ── scan_expired_files ─────────────────────────────────────────

  Scenario: scan_expired_files skips entry that is not a file
    Given cleanup coverage has a CleanupService with default settings
    And cleanup coverage has a temp directory with a subdirectory named "notafile.log"
    When cleanup coverage scans expired files in that directory
    Then cleanup coverage expired files result should be empty

  Scenario: scan_expired_files skips entry when stat raises OSError
    Given cleanup coverage has a CleanupService with default settings
    When cleanup coverage scans expired files where stat raises OSError
    Then cleanup coverage expired files result should be empty

  # ── _purge_logs ────────────────────────────────────────────────

  Scenario: _purge_logs increments skipped when unlink fails
    Given cleanup coverage has a CleanupService with log dir containing expired files
    When cleanup coverage purges logs and unlink raises OSError
    Then cleanup coverage purge logs report should show skipped count of 1

  # ── _purge_backups ─────────────────────────────────────────────

  Scenario: _purge_backups increments skipped when unlink fails
    Given cleanup coverage has a CleanupService with backup dir containing expired files
    When cleanup coverage purges backups and unlink raises OSError
    Then cleanup coverage purge backups report should show skipped count of 1

  # ── _age_description ───────────────────────────────────────────

  Scenario: _age_description returns unknown age when stat raises OSError
    When cleanup coverage calls _age_description on a path where stat raises OSError
    Then cleanup coverage age description should be "unknown age"
