Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions codec_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@
"cadence_idle_s": 300,
"idle_threshold_s": 60,
"buffer_depth_min": 10,
# OCR enable flag (2026-05-02 hotfix). Default True preserves Step 5
# design behavior on machines where Screen Recording permission is
# granted to the python3.13 process running codec-observer. Set false
# to skip the screencapture+Vision call entirely — buffer still gets
# active_window + clipboard + recent_files signals, just no OCR.
# Flipping this to false is the recommended workaround for the macOS
# popup-storm bug when permissions aren't granted to the PM2 child:
# screencapture blocks subprocess.run waiting for the popup AND the
# ThreadPoolExecutor's `with` exit waits for the thread to finish,
# so each poll generates ~2 popups + ~5s of blocking until dismissal.
# See ~/.codec/config.json:observer.ocr_enabled to override.
"ocr_enabled": True,
"ocr_timeout_ms": 100,
"ocr_retry_timeout_ms": 200, # Q5.1
"reset_on_long_idle": True,
Expand Down Expand Up @@ -514,9 +526,16 @@ def poll(buffer: Optional[RingBuffer] = None,
"content_type": _classify_clipboard_kind(cb_now),
}

# 3. Screenshot OCR (with retry per Q5.1)
ocr_text, ocr_skipped = _get_screenshot_ocr(
int(cfg["ocr_timeout_ms"]), int(cfg["ocr_retry_timeout_ms"]))
# 3. Screenshot OCR (with retry per Q5.1).
# 2026-05-02 hotfix: bypass entirely when ocr_enabled=False to avoid
# the screencapture popup storm on machines without Screen Recording
# permission granted to the PM2 child process. Buffer still gets
# active_window + clipboard + recent_files signals.
if cfg.get("ocr_enabled", True):
ocr_text, ocr_skipped = _get_screenshot_ocr(
int(cfg["ocr_timeout_ms"]), int(cfg["ocr_retry_timeout_ms"]))
else:
ocr_text, ocr_skipped = ("", True)

# 4. Recent files
recent_files = _get_recent_files(window_seconds=300)
Expand Down
58 changes: 58 additions & 0 deletions tests/test_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,61 @@ def test_inject_empty_buffer_returns_skipped(monkeypatch, temp_audit_log):
# No audit emit on skipped
recs = _records(temp_audit_log)
assert _events_of(recs, codec_audit.OBSERVATION_SUMMARY_INJECTED) == []


# ─────────────────────────────────────────────────────────────────────────────
# 2026-05-02 hotfix — ocr_enabled flag (popup storm mitigation)
# ─────────────────────────────────────────────────────────────────────────────

def test_ocr_enabled_false_skips_screenshot_call(fresh_buffer, temp_audit_log,
monkeypatch):
"""When config.observer.ocr_enabled=False, poll() must NOT call
_get_screenshot_ocr. Verified by sentinel that raises if called."""
cfg = dict(codec_observer._DEFAULT_CONFIG)
cfg["ocr_enabled"] = False
monkeypatch.setattr(codec_observer, "_idle_seconds", lambda: 0.0)
monkeypatch.setattr(codec_observer, "_get_active_window", lambda: {})
monkeypatch.setattr(codec_observer, "_get_clipboard_now", lambda: "")
monkeypatch.setattr(codec_observer, "_get_recent_files",
lambda window_seconds=300: [])

def _should_not_be_called(*a, **kw):
raise AssertionError("_get_screenshot_ocr called despite ocr_enabled=False")
monkeypatch.setattr(codec_observer, "_get_screenshot_ocr",
_should_not_be_called)

snap = codec_observer.poll(buffer=fresh_buffer, cfg=cfg, emit_audit=True)
assert snap["ocr_skipped"] is True
assert snap["screenshot_ocr"] == ""
# Audit emit still records the skipped state correctly
recs = _records(temp_audit_log)
extra = _events_of(recs, codec_audit.OBSERVATION_TICK)[0]["extra"]
assert extra["ocr_skipped"] is True
assert extra["ocr_chars"] == 0


def test_ocr_enabled_default_is_true():
"""Default config has ocr_enabled=True (preserves Step 5 behavior
on properly-permissioned machines)."""
assert codec_observer._DEFAULT_CONFIG["ocr_enabled"] is True


def test_ocr_enabled_true_calls_screenshot(fresh_buffer, temp_audit_log,
monkeypatch):
"""Sanity check: when ocr_enabled=True (default), _get_screenshot_ocr
IS called. Confirms the new flag isn't accidentally always-bypassing."""
cfg = dict(codec_observer._DEFAULT_CONFIG)
assert cfg["ocr_enabled"] is True
monkeypatch.setattr(codec_observer, "_idle_seconds", lambda: 0.0)
monkeypatch.setattr(codec_observer, "_get_active_window", lambda: {})
monkeypatch.setattr(codec_observer, "_get_clipboard_now", lambda: "")
monkeypatch.setattr(codec_observer, "_get_recent_files",
lambda window_seconds=300: [])
call_count = [0]

def _track_call(*a, **kw):
call_count[0] += 1
return ("", True) # mocked skip — doesn't actually screencapture
monkeypatch.setattr(codec_observer, "_get_screenshot_ocr", _track_call)
codec_observer.poll(buffer=fresh_buffer, cfg=cfg, emit_audit=False)
assert call_count[0] == 1
Loading