forked from Rockachopa/Timmy-time-dashboard
321 lines
10 KiB
Python
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"
|