This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/timmy/test_workspace.py

321 lines
10 KiB
Python

"""Tests for timmy.workspace — Workspace heartbeat monitoring."""
from timmy.workspace import WorkspaceMonitor
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_monitor(tmp_path, monkeypatch):
"""Create a WorkspaceMonitor with tmp_path as the repo_root."""
# Mock repo_root to use tmp_path
monkeypatch.setattr(
"timmy.workspace.settings", type("obj", (object,), {"repo_root": str(tmp_path)})()
)
state_path = tmp_path / "workspace_state.json"
return WorkspaceMonitor(state_path=state_path)
def _setup_workspace(tmp_path):
"""Create the workspace directory structure."""
workspace = tmp_path / "workspace"
workspace.mkdir()
(workspace / "inbox").mkdir()
(workspace / "outbox").mkdir()
return workspace
# ---------------------------------------------------------------------------
# Basic monitoring
# ---------------------------------------------------------------------------
def test_no_updates_when_empty(tmp_path, monkeypatch):
"""Fresh monitor with no workspace files should report no updates."""
# Don't create workspace dir — monitor should handle gracefully
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] is None
assert updates["new_inbox_files"] == []
def test_detects_new_correspondence(tmp_path, monkeypatch):
"""Writing to correspondence.md should be detected as new entries."""
workspace = _setup_workspace(tmp_path)
correspondence = workspace / "correspondence.md"
# Pre-populate correspondence file
correspondence.write_text("Entry 1\nEntry 2\nEntry 3\n")
monitor = _make_monitor(tmp_path, monkeypatch)
# Should detect all 3 lines as new
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] == "Entry 1\nEntry 2\nEntry 3"
assert updates["new_inbox_files"] == []
def test_detects_new_inbox_file(tmp_path, monkeypatch):
"""Creating a file in inbox/ should be detected as new."""
workspace = _setup_workspace(tmp_path)
inbox = workspace / "inbox"
# Create a file in inbox
(inbox / "message_1.md").write_text("Hello Timmy")
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] is None
assert updates["new_inbox_files"] == ["message_1.md"]
def test_detects_multiple_inbox_files(tmp_path, monkeypatch):
"""Multiple new inbox files should all be detected."""
workspace = _setup_workspace(tmp_path)
inbox = workspace / "inbox"
# Create multiple files
(inbox / "message_2.md").write_text("Hello again")
(inbox / "task_1.md").write_text("Do something")
(inbox / "note.txt").write_text("A note")
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_inbox_files"] == ["message_2.md", "note.txt", "task_1.md"]
def test_detects_both_correspondence_and_inbox(tmp_path, monkeypatch):
"""Monitor should detect both correspondence and inbox updates together."""
workspace = _setup_workspace(tmp_path)
correspondence = workspace / "correspondence.md"
correspondence.write_text("New journal entry\n")
inbox = workspace / "inbox"
(inbox / "urgent.md").write_text("Urgent message")
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] == "New journal entry"
assert updates["new_inbox_files"] == ["urgent.md"]
# ---------------------------------------------------------------------------
# Marking as seen
# ---------------------------------------------------------------------------
def test_mark_seen_clears_pending(tmp_path, monkeypatch):
"""After mark_seen, get_pending_updates should return empty."""
workspace = _setup_workspace(tmp_path)
correspondence = workspace / "correspondence.md"
correspondence.write_text("Line 1\nLine 2\n")
inbox = workspace / "inbox"
(inbox / "file.md").write_text("Content")
monitor = _make_monitor(tmp_path, monkeypatch)
# First check — should have updates
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] is not None
assert len(updates["new_inbox_files"]) == 1
# Mark as seen
monitor.mark_seen()
# Second check — should be empty
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] is None
assert updates["new_inbox_files"] == []
def test_mark_seen_persists_line_count(tmp_path, monkeypatch):
"""mark_seen should remember how many lines we've seen."""
workspace = _setup_workspace(tmp_path)
correspondence = workspace / "correspondence.md"
correspondence.write_text("Line 1\nLine 2\nLine 3\n")
monitor = _make_monitor(tmp_path, monkeypatch)
monitor.mark_seen()
# Add more lines
correspondence.write_text("Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n")
# Should only see the new lines
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] == "Line 4\nLine 5"
# ---------------------------------------------------------------------------
# State persistence
# ---------------------------------------------------------------------------
def test_state_persists_across_instances(tmp_path, monkeypatch):
"""State should be saved and loaded when creating a new monitor instance."""
workspace = _setup_workspace(tmp_path)
correspondence = workspace / "correspondence.md"
correspondence.write_text("First entry\n")
inbox = workspace / "inbox"
(inbox / "first.md").write_text("First")
# First monitor instance
monitor1 = _make_monitor(tmp_path, monkeypatch)
monitor1.mark_seen()
# Add new content
correspondence.write_text("First entry\nSecond entry\n")
(inbox / "second.md").write_text("Second")
# Second monitor instance (should load state from file)
monitor2 = _make_monitor(tmp_path, monkeypatch)
updates = monitor2.get_pending_updates()
assert updates["new_correspondence"] == "Second entry"
assert updates["new_inbox_files"] == ["second.md"]
def test_state_survives_missing_files(tmp_path, monkeypatch):
"""Monitor should handle missing correspondence file gracefully."""
workspace = _setup_workspace(tmp_path)
# Create and mark as seen
correspondence = workspace / "correspondence.md"
correspondence.write_text("Entry\n")
monitor = _make_monitor(tmp_path, monkeypatch)
monitor.mark_seen()
# Delete the file
correspondence.unlink()
# Should return None gracefully
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] is None
# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------
def test_empty_correspondence_file(tmp_path, monkeypatch):
"""Empty correspondence file should return None."""
workspace = _setup_workspace(tmp_path)
(workspace / "correspondence.md").write_text("")
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] is None
def test_empty_inbox_dir(tmp_path, monkeypatch):
"""Empty inbox directory should return empty list."""
_setup_workspace(tmp_path)
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_inbox_files"] == []
def test_missing_inbox_dir(tmp_path, monkeypatch):
"""Missing inbox directory should return empty list."""
workspace = tmp_path / "workspace"
workspace.mkdir()
# No inbox subdir
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_inbox_files"] == []
def test_missing_workspace_dir(tmp_path, monkeypatch):
"""Missing workspace directory should return empty results."""
# No workspace dir at all
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] is None
assert updates["new_inbox_files"] == []
def test_correspondence_with_blank_lines(tmp_path, monkeypatch):
"""Correspondence with blank lines should be handled correctly."""
workspace = _setup_workspace(tmp_path)
correspondence = workspace / "correspondence.md"
correspondence.write_text("Entry 1\n\nEntry 2\n\n\nEntry 3\n")
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] == "Entry 1\n\nEntry 2\n\n\nEntry 3"
def test_inbox_ignores_subdirectories(tmp_path, monkeypatch):
"""Inbox should only list files, not subdirectories."""
workspace = _setup_workspace(tmp_path)
inbox = workspace / "inbox"
(inbox / "file.md").write_text("Content")
(inbox / "subdir").mkdir()
monitor = _make_monitor(tmp_path, monkeypatch)
updates = monitor.get_pending_updates()
assert updates["new_inbox_files"] == ["file.md"]
def test_deleted_inbox_files_removed_from_state(tmp_path, monkeypatch):
"""When inbox files are deleted, they should be removed from seen list."""
workspace = _setup_workspace(tmp_path)
inbox = workspace / "inbox"
# Create and see a file
(inbox / "temp.md").write_text("Temp")
monitor = _make_monitor(tmp_path, monkeypatch)
monitor.mark_seen()
# Delete the file
(inbox / "temp.md").unlink()
# mark_seen should update seen list to remove deleted files
monitor.mark_seen()
# State should now have empty seen list
assert monitor._state["seen_inbox_files"] == []
def test_correspondence_append_only(tmp_path, monkeypatch):
"""Correspondence is append-only; modifying existing content doesn't re-notify."""
workspace = _setup_workspace(tmp_path)
correspondence = workspace / "correspondence.md"
correspondence.write_text("Line 1\nLine 2\n")
monitor = _make_monitor(tmp_path, monkeypatch)
monitor.mark_seen()
# Modify the file (truncate and rewrite) — this resets line count
# but correspondence.md should be append-only in practice
correspondence.write_text("Modified Line 1\nModified Line 2\nLine 3\n")
# Line 3 is the only truly new line (we've now seen 2, file has 3)
updates = monitor.get_pending_updates()
assert updates["new_correspondence"] == "Line 3"