← refacto-feature-scout
"""Leaving it running: one job at a time, and a scan a day even if the laptop was asleep."""
from datetime import datetime
import pytest
from scout.locks import Busy, Lock
from scout.projects import Project
from scout.watch import due_for_a_scan, watch
PROJECT = Project(key="terminal", name="Test App", folder="/tmp/app")
def at(day: int, hour: int, minute: int = 0) -> float:
return datetime(2026, 9, day, hour, minute).timestamp()
# ---- one job at a time ----
def test_a_second_run_is_told_to_wait_its_turn(tmp_path):
path = tmp_path / "one.lock"
with Lock(path=path):
with pytest.raises(Busy) as e:
with Lock(path=path):
pass
assert "already busy" in str(e.value)
for jargon in ("lock", "flock", "mutex"):
assert jargon not in str(e.value).lower()
def test_the_lock_is_let_go_afterwards(tmp_path):
path = tmp_path / "one.lock"
with Lock(path=path):
pass
with Lock(path=path):
pass # no complaint the second time
def test_waiting_gives_up_after_the_time_allowed(tmp_path):
path = tmp_path / "one.lock"
ticks = iter([0, 5, 10, 100])
with Lock(path=path):
with pytest.raises(Busy):
with Lock(path=path, wait_s=30, sleep=lambda s: None, clock=lambda: next(ticks)):
pass
# ---- a scan a day, by the clock rather than a timer ----
def test_nothing_happens_before_the_hour(store):
assert not due_for_a_scan(store, PROJECT, at(6, 7, 30), hour=8, minute=15)
assert due_for_a_scan(store, PROJECT, at(6, 8, 15), hour=8, minute=15)
def test_a_laptop_that_was_asleep_catches_up(store):
"""The hour passed while the machine was off; opening it later must still scan."""
assert due_for_a_scan(store, PROJECT, at(6, 14, 0), hour=8, minute=15)
def test_it_does_not_scan_twice_in_a_day(store):
import time
run = store.start_run("scan:terminal")
store.finish_run(run, True, {"project": "terminal"})
# hour 0 so only the "already done today" part of the decision is under test
assert not due_for_a_scan(store, PROJECT, time.time(), hour=0, minute=0)
# ---- the loop itself ----
def run_once(store, *, now, scanned, polled, **kw):
watch([PROJECT], store, scanner=lambda st, ps: scanned.append([p.key for p in ps]),
poller=lambda st: polled.append(1), say=lambda m: None,
now=lambda: now, sleep=lambda s: None, once=True, **kw)
def test_a_pass_looks_for_ideas_and_gets_on_with_the_work(store):
scanned, polled = [], []
run_once(store, now=at(6, 9, 0), scanned=scanned, polled=polled)
assert scanned == [["terminal"]] and polled == [1]
def test_the_work_carries_on_even_when_it_is_not_time_to_look_for_ideas(store):
scanned, polled = [], []
run_once(store, now=at(6, 6, 0), scanned=scanned, polled=polled)
assert scanned == [] and polled == [1]
def test_a_pass_steps_aside_when_something_else_is_running(store, tmp_path, monkeypatch):
import scout.watch as watchmod
lock_file = tmp_path / "busy.lock"
monkeypatch.setattr(watchmod, "Lock", lambda **kw: Lock(path=lock_file, **kw))
scanned, polled = [], []
with Lock(path=lock_file):
run_once(store, now=at(6, 9, 0), scanned=scanned, polled=polled)
assert scanned == [] and polled == [] # skipped quietly, not crashed