Files
roboco/tests/unit/services/test_workspace_prune_broken_refs.py
Renn F 718a16c4f7 Close residual gaps: CEO escalation from blocked, ref repair, reset-script state
Three independent residual hardenings surfaced by the smoke run:

- escalate_to_ceo is now reachable from a blocked task (source widened to
  {awaiting_pm_review, blocked} with a matching status-transition row), so a
  main_pm or board agent has a clean verb to surface a task it cannot resolve
  to the CEO — who can then approve, reject, or cancel it. Regenerated the
  lifecycle artifacts (status-transitions doc + panel JSON) to match.

- WorkspaceService now prunes broken loose refs (.bak debris and any ref whose
  contents are neither an object id nor a symref) before the refresh fetch, so
  a ref left corrupt by an interrupted recovery no longer produces per-operation
  "broken ref" warnings or wedges ref enumeration. Best-effort, file-reads-only.

- The reset script's full-reset Claude-state clear now defaults to the
  replay-state subdirs (projects/, todos/) of the mounted Claude home and
  refuses to clear the home root, so a full reset actually clears conversation
  replay state by default without wiping the host's stored credentials.
2026-06-08 05:17:15 +02:00

65 lines
2.0 KiB
Python

"""`WorkspaceService._prune_broken_refs` drops debris loose refs before a fetch.
Interrupted hard-stop recovery can leave `.bak` ref debris and truncated/garbage
loose-ref files under `.git/refs`. The prune reads files only (no git subprocess),
so these tests just stage a `.git/refs` tree on disk.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from roboco.services.workspace import WorkspaceService
if TYPE_CHECKING:
from pathlib import Path
_VALID_SHA1 = "a" * 40
_VALID_SHA256 = "b" * 64
def _make_heads(workspace: Path) -> Path:
heads = workspace / ".git" / "refs" / "heads"
heads.mkdir(parents=True)
return heads
def test_keeps_valid_sha1_ref(tmp_path: Path) -> None:
good = _make_heads(tmp_path) / "main"
good.write_text(_VALID_SHA1 + "\n", encoding="utf-8")
WorkspaceService._prune_broken_refs(tmp_path)
assert good.exists()
def test_keeps_valid_sha256_ref(tmp_path: Path) -> None:
good = _make_heads(tmp_path) / "main"
good.write_text(_VALID_SHA256 + "\n", encoding="utf-8")
WorkspaceService._prune_broken_refs(tmp_path)
assert good.exists()
def test_keeps_symref(tmp_path: Path) -> None:
sym = _make_heads(tmp_path) / "current"
sym.write_text("ref: refs/heads/main\n", encoding="utf-8")
WorkspaceService._prune_broken_refs(tmp_path)
assert sym.exists()
def test_removes_bak_debris_even_with_valid_content(tmp_path: Path) -> None:
bak = _make_heads(tmp_path) / "feature.bak"
bak.write_text(_VALID_SHA1, encoding="utf-8")
WorkspaceService._prune_broken_refs(tmp_path)
assert not bak.exists()
def test_removes_garbage_ref(tmp_path: Path) -> None:
junk = _make_heads(tmp_path) / "broken"
junk.write_text("not-a-sha-or-symref garbage", encoding="utf-8")
WorkspaceService._prune_broken_refs(tmp_path)
assert not junk.exists()
def test_no_git_refs_dir_is_noop(tmp_path: Path) -> None:
# No .git/refs present — must not raise.
WorkspaceService._prune_broken_refs(tmp_path)