Files
automaton/tests/test_cleanup_done.py
Lap Tran bc7daf8590 Restore archived tasks, fix dashboard scroll-reset, bind ornith, add Playwright smoke test
- **Restore 82 completed tasks** from tasks/complete/ back to tasks/ top
  level (all <7 days old per the cleanup policy; premature bulk archive
  was fixed).
- **Dashboard: fix scroll-reset on auto-refresh** — renderBoard rebuilds
  the board via innerHTML every 2s, destroying each column-body's
  scrollTop. Now snapshots column-body scrollTop + board.scrollLeft +
  view.scrollTop before rebuild and restores after (matched by
  PHASE_GROUPS index).
- **Dashboard UI additions** (pre-existing unstaged work): approval
  section cards, transition buttons, inline artifact editor (textarea for
  writing missing SPEC/VERDICT/etc from the detail modal).
- **Bind ornith as Implement model** — config.md: Model explicit to
  omlx/Ornith-1.0-35B-4bit-mlx, context window 32768. Interactive
  autopilot already used ornith via opencode default; now explicit.
- **Fix cleanup stub** — automaton-cleanup.sh had a stale --project arg
  pointing at a pytest temp dir (test isolation leak). Rewired to point
  at ~/.automaton.
- **Fix plist-isolation test** — test asserted host plist doesn't exist,
  but a real install creates it. Now snapshots mtime before run, asserts
  unchanged after (only a write during the test counts as bleed).
- **New Playwright smoke test** (tests/test_dashboard_ui.py) — 2 tests:
  board renders tasks, column scroll survives auto-refresh tick.
  Verified the test fails without the scroll fix (scrollTop resets to 0).
  Skipped via importorskip when playwright is absent (main CI stays
  green).
- **Clarify SI loop scope in README** — new-project onboarding section
  documents the framework-scoped self-improvement loop and options
  (leave/pause/create project loop).
- **CHANGELOG** documents all changes including the known model-divergence
  gap (mde tasks marked complete but per-role model binding was never
  implemented).
2026-06-26 10:05:18 -04:00

166 lines
6.7 KiB
Python

"""Tests for the task cleanup process (--cleanup-done) and its scheduled install.
Covers:
- cmd_cleanup_done: age threshold (by .state mtime), move, dry-run, skip
(dest exists, untracked, non-complete, below threshold), --json summary.
- --install-cleanup-schedule: stub + launchd plist written to
AUTOMATON_LAUNCHAGENTS_DIR override (no host bleed).
- _all_task_dirs excludes the 'complete' container (so --audit/--list stay
clean once tasks are archived).
"""
import importlib.util
import json
import os
import subprocess
import sys
import time
from pathlib import Path
import pytest
_STATUS_PATH = Path.home() / ".automaton" / "scripts" / "status.py"
_spec = importlib.util.spec_from_file_location("st_cleanup", _STATUS_PATH)
st = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(st)
def _task_ns(days=None, dry_run=False, json_out=False, project=None):
class A:
pass
a = A()
a.days = days
a.dry_run = dry_run
a.json_output = json_out
a.project = project
return a
def _make_complete_task(project_root: Path, name: str, age_days: float = 0.0) -> Path:
task_dir = project_root / ".automaton" / "tasks" / name
task_dir.mkdir(parents=True)
(task_dir / ".state").write_text("complete\n")
(task_dir / "VERDICT.md").write_text("## Status: PASS\n")
if age_days:
when = time.time() - age_days * 86400.0
os.utime(task_dir / ".state", (when, when))
return task_dir
@pytest.fixture
def tmp_project(tmp_path):
(tmp_path / ".automaton" / "tasks").mkdir(parents=True)
return tmp_path
class TestCleanupDone:
def test_moves_old_complete_task(self, tmp_project):
_make_complete_task(tmp_project, "old-task", age_days=8.0)
rc = st.cmd_cleanup_done(_task_ns(days=7, project=str(tmp_project)))
assert rc == 0
dest = tmp_project / ".automaton" / "tasks" / "complete" / "old-task"
assert dest.exists()
assert (dest / ".state").exists()
def test_keeps_recent_complete_task(self, tmp_project):
td = _make_complete_task(tmp_project, "fresh-task", age_days=1.0)
st.cmd_cleanup_done(_task_ns(days=7, project=str(tmp_project)))
assert td.exists() # not moved
assert not (tmp_project / ".automaton" / "tasks" / "complete" / "fresh-task").exists()
def test_dry_run_does_not_move(self, tmp_project):
td = _make_complete_task(tmp_project, "dry-task", age_days=10.0)
rc = st.cmd_cleanup_done(_task_ns(days=7, dry_run=True, project=str(tmp_project)))
assert rc == 0
assert td.exists() # still in place
assert not (tmp_project / ".automaton" / "tasks" / "complete").exists()
def test_skip_untracked(self, tmp_project):
# dir exists but no .state -> untracked, ignored
untracked = tmp_project / ".automaton" / "tasks" / "no-state"
untracked.mkdir(parents=True)
rc = st.cmd_cleanup_done(_task_ns(days=0, project=str(tmp_project)))
assert rc == 0
assert untracked.exists() # untouched
def test_skip_non_complete(self, tmp_project):
td = tmp_project / ".automaton" / "tasks" / "wip"
td.mkdir(parents=True)
(td / ".state").write_text("implement\n")
rc = st.cmd_cleanup_done(_task_ns(days=0, project=str(tmp_project)))
assert rc == 0
assert td.exists()
assert (td / ".state").read_text().strip() == "implement"
def test_skip_when_dest_exists(self, tmp_project, capsys):
_make_complete_task(tmp_project, "dup", age_days=9.0)
dest = tmp_project / ".automaton" / "tasks" / "complete" / "dup"
dest.mkdir(parents=True)
(dest / ".state").write_text("complete\n")
rc = st.cmd_cleanup_done(_task_ns(days=7, project=str(tmp_project)))
assert rc == 0
out = capsys.readouterr().out
assert "skipped" in out and "dest_exists" in out
def test_json_summary(self, tmp_project, capsys):
_make_complete_task(tmp_project, "json-old", age_days=9.0)
_make_complete_task(tmp_project, "json-new", age_days=1.0)
st.cmd_cleanup_done(_task_ns(days=7, json_out=True, project=str(tmp_project)))
out = capsys.readouterr().out
last = out.strip().splitlines()[-1]
summary = json.loads(last)
assert summary["days"] == 7
assert summary["dry_run"] is False
assert "json-old" in summary["moved"]
assert "json-new" not in summary["moved"]
def test_all_task_dirs_excludes_complete_container(self, tmp_project):
_make_complete_task(tmp_project, "active-done", age_days=0.0)
archive = tmp_project / ".automaton" / "tasks" / "complete" / "archived"
archive.mkdir(parents=True)
names = {n for n, _ in st._all_task_dirs(str(tmp_project))}
assert "active-done" in names
assert "complete" not in names
assert "archived" not in names
class TestInstallCleanupScheduleIsolation:
def _run_cli(self, args, env_dir):
env = os.environ.copy()
env["AUTOMATON_LAUNCHAGENTS_DIR"] = str(env_dir)
cmd = [sys.executable, str(_STATUS_PATH)] + args
return subprocess.run(cmd, capture_output=True, text=True, env=env)
def test_plist_written_to_override_dir_not_host(self, tmp_path):
agents = tmp_path / "agents"
agents.mkdir()
host = Path.home() / "Library" / "LaunchAgents" / "com.automaton.cleanup.plist"
# Snapshot host plist state before the run. A pre-existing real install
# is OK — only a write during this test would be a bleed.
host_before = host.stat().st_mtime_ns if host.exists() else None
res = self._run_cli(
["--install-cleanup-schedule", "--days", "5", "--interval", "120",
"--project", str(tmp_path)],
agents,
)
assert res.returncode == 0, res.stdout + res.stderr
plist = agents / "com.automaton.cleanup.plist"
assert plist.exists()
host_after = host.stat().st_mtime_ns if host.exists() else None
assert host_before == host_after, "host plist was modified by the test run"
content = plist.read_text()
assert "com.automaton.cleanup" in content
assert "<integer>120</integer>" in content
stub = Path(st.AUTOMATON_DIR) / "scripts" / "automaton-cleanup.sh"
assert stub.exists()
assert "--days 5" in stub.read_text()
@pytest.mark.skipif(sys.platform != "darwin", reason="Darwin plist branch")
def test_uninstall_via_disabled_rename(self, tmp_path):
agents = tmp_path / "agents"
agents.mkdir()
res = self._run_cli(
["--install-cleanup-schedule", "--project", str(tmp_path)], agents)
assert res.returncode == 0
plist = agents / "com.automaton.cleanup.plist"
assert plist.exists()