personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_detect_objects.py
4.7 kB 158 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4import json 5import logging 6import subprocess 7from pathlib import Path 8 9import pytest 10 11from solstone.observe import detect 12from solstone.think.providers.rfdetr_install import ( 13 ENGINE_REF, 14 MODEL_NAME, 15 RfdetrPaths, 16) 17 18 19@pytest.fixture(autouse=True) 20def _reset_detect_state(): 21 detect._disabled = False 22 yield 23 detect._disabled = False 24 25 26def _canned_cli_json() -> dict: 27 return { 28 "image": {"width": 100, "height": 50}, 29 "detections": [ 30 { 31 "class_id": 1, 32 "class_name": "cup", 33 "score": 0.72, 34 "bbox": [1, 2, 3, 4], 35 } 36 ], 37 } 38 39 40def test_detect_objects_missing_provider_latches_without_reattempt(monkeypatch, caplog): 41 calls = 0 42 43 def fake_paths(): 44 nonlocal calls 45 calls += 1 46 return RfdetrPaths(status="not_installed") 47 48 monkeypatch.setattr(detect, "rfdetr_paths", fake_paths) 49 caplog.set_level(logging.WARNING, logger=detect.LOG.name) 50 51 assert detect.detect_objects(b"x") is None 52 assert detect.detect_objects(b"x") is None 53 54 warnings = [ 55 record 56 for record in caplog.records 57 if record.name == detect.LOG.name and record.levelno == logging.WARNING 58 ] 59 assert detect._disabled is True 60 assert len(warnings) == 1 61 assert warnings[0].getMessage() == ( 62 "object detection disabled: rf-detr provider not_installed" 63 ) 64 assert calls == 1 65 66 67def test_detect_objects_returns_parsed_cli_json(monkeypatch, tmp_path): 68 canned = _canned_cli_json() 69 argv_seen = [] 70 71 monkeypatch.setattr( 72 detect, 73 "rfdetr_paths", 74 lambda: RfdetrPaths( 75 status="installed", 76 binary_path=tmp_path / "rfdetr-cli", 77 model_path=tmp_path / "model.gguf", 78 ), 79 ) 80 81 def fake_run(argv, **kwargs): 82 argv_seen.append((argv, kwargs)) 83 output_path = Path(argv[argv.index("--output") + 1]) 84 output_path.write_text(json.dumps(canned), encoding="utf-8") 85 return subprocess.CompletedProcess(argv, 0) 86 87 monkeypatch.setattr(detect.subprocess, "run", fake_run) 88 89 assert detect.detect_objects(b"png-bytes") == canned 90 assert len(argv_seen) == 1 91 argv, kwargs = argv_seen[0] 92 assert argv[1] == "detect" 93 assert argv[argv.index("--threshold") + 1] == str(detect.THRESHOLD) 94 assert kwargs["timeout"] == detect._TIMEOUT_S 95 assert kwargs["capture_output"] is True 96 assert kwargs["check"] is True 97 98 99def test_detect_objects_subprocess_failure_latches(monkeypatch, tmp_path): 100 monkeypatch.setattr( 101 detect, 102 "rfdetr_paths", 103 lambda: RfdetrPaths( 104 status="installed", 105 binary_path=tmp_path / "rfdetr-cli", 106 model_path=tmp_path / "model.gguf", 107 ), 108 ) 109 110 def fake_run(argv, **_kwargs): 111 raise subprocess.CalledProcessError(1, argv, stderr=b"bad") 112 113 monkeypatch.setattr(detect.subprocess, "run", fake_run) 114 115 assert detect.detect_objects(b"png-bytes") is None 116 assert detect._disabled is True 117 118 119def test_detections_block_renames_and_preserves_provenance(): 120 canned = _canned_cli_json() 121 122 assert detect.detections_block(canned, source="screen", gate="primary:media") == { 123 "engine": detect.ENGINE_NAME, 124 "engine_ref": ENGINE_REF, 125 "model": MODEL_NAME, 126 "threshold": detect.THRESHOLD, 127 "source": "screen", 128 "gate": "primary:media", 129 "image": canned["image"], 130 "objects": canned["detections"], 131 } 132 133 134def test_screen_gate_prefers_primary_gate(): 135 assert detect.screen_gate({"primary": "media", "secondary": "none"}) == ( 136 "primary:media" 137 ) 138 assert detect.screen_gate({"primary": "code", "secondary": "social"}) == ( 139 "secondary:social" 140 ) 141 assert detect.screen_gate({"primary": "media", "secondary": "social"}) == ( 142 "primary:media" 143 ) 144 assert detect.screen_gate({"primary": "code", "secondary": "terminal"}) is None 145 146 147def test_qualified_objects_filters_at_read_time_only(): 148 tv = {"class_name": "tv", "score": 0.7} 149 weak_person = {"class_name": "person", "score": 0.39} 150 person = {"class_name": "person", "score": 0.41} 151 cup = {"class_name": "cup", "score": 0.41} 152 sandwich = {"class_name": "sandwich", "score": 0.9} 153 154 assert detect.qualified_objects({"source": "screen", "objects": [tv]}) == [] 155 assert detect.qualified_objects({"source": "still", "objects": [tv]}) == [tv] 156 assert detect.qualified_objects( 157 {"source": "still", "objects": [weak_person, person, cup, sandwich]} 158 ) == [person, cup]