From 16e4951814f7e907feb19a8016dcac36ec3b7283 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:42:48 +0000 Subject: [PATCH 1/8] Initial plan From a142e8da8392cfb83e643f0ea53b8f8c0cfe5a91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:46:31 +0000 Subject: [PATCH 2/8] Avoid eager tracker import requiring cv2 in memory tests --- services/tracking/__init__.py | 11 ++++++++++- tests/test_memory.py | 21 ++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/services/tracking/__init__.py b/services/tracking/__init__.py index de724e0..0b0c012 100644 --- a/services/tracking/__init__.py +++ b/services/tracking/__init__.py @@ -4,4 +4,13 @@ modules so that lightweight consumers (tests, memory service) can import sub-modules like ``cross_camera_reid`` without pulling in the full stack. """ -__all__: list[str] = [] +from importlib import import_module +from types import ModuleType + +__all__ = ["tracker"] + + +def __getattr__(name: str) -> ModuleType: + if name == "tracker": + return import_module("services.tracking.tracker") + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/test_memory.py b/tests/test_memory.py index 09b9b93..1a3a151 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -7,6 +7,8 @@ import sys import os import time +import builtins +import importlib sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import pytest @@ -48,6 +50,24 @@ def test_track_event_serialises_cleanly(): assert ActionHint.WALKING.value == "walking" +def test_memory_import_does_not_require_cv2(monkeypatch): + """Importing memory service should not eagerly import cv2-dependent tracker.""" + real_import = builtins.__import__ + + def guarded_import(name, *args, **kwargs): + if name == "cv2": + raise ModuleNotFoundError("No module named 'cv2'") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", guarded_import) + monkeypatch.delitem(sys.modules, "services.tracking", raising=False) + monkeypatch.delitem(sys.modules, "services.tracking.tracker", raising=False) + monkeypatch.delitem(sys.modules, "services.memory.memory", raising=False) + + imported = importlib.import_module("services.memory.memory") + assert hasattr(imported, "MemoryStore") + + def test_track_sequence_action_summary(): seq = TrackSequence( track_id = 1, @@ -407,4 +427,3 @@ def test_reasoning_result_id_present_after_set(store): store.store_event(evt) seq = store.get_sequence(track_id=51) assert seq.events[0].reasoning_result_id == "test-alert-id-123" - From 1c880e4a303e137cf2e34cfbef340746c973ced1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:47:29 +0000 Subject: [PATCH 3/8] Assert cv2 remains unloaded in memory import regression test --- tests/test_memory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_memory.py b/tests/test_memory.py index 1a3a151..ca14c3c 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -66,6 +66,7 @@ def guarded_import(name, *args, **kwargs): imported = importlib.import_module("services.memory.memory") assert hasattr(imported, "MemoryStore") + assert "cv2" not in sys.modules def test_track_sequence_action_summary(): From e8123410d86e9cf63b299895c1f36c15bb440c46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:48:29 +0000 Subject: [PATCH 4/8] Strengthen cv2 import regression test assertions --- tests/test_memory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_memory.py b/tests/test_memory.py index ca14c3c..f1aaf69 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -67,6 +67,8 @@ def guarded_import(name, *args, **kwargs): imported = importlib.import_module("services.memory.memory") assert hasattr(imported, "MemoryStore") assert "cv2" not in sys.modules + store = imported.MemoryStore(redis_client=fakeredis.FakeRedis(decode_responses=True)) + assert isinstance(store, imported.MemoryStore) def test_track_sequence_action_summary(): From 2bed0d94df5381ac36fbbd49bc94893fc4f1cfed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:49:23 +0000 Subject: [PATCH 5/8] Refine lazy tracking import and cv2 regression coverage --- services/tracking/__init__.py | 2 +- tests/test_memory.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/services/tracking/__init__.py b/services/tracking/__init__.py index 0b0c012..6544b6b 100644 --- a/services/tracking/__init__.py +++ b/services/tracking/__init__.py @@ -7,7 +7,7 @@ from importlib import import_module from types import ModuleType -__all__ = ["tracker"] +__all__: list[str] = [] def __getattr__(name: str) -> ModuleType: diff --git a/tests/test_memory.py b/tests/test_memory.py index f1aaf69..6fe98c3 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -69,6 +69,7 @@ def guarded_import(name, *args, **kwargs): assert "cv2" not in sys.modules store = imported.MemoryStore(redis_client=fakeredis.FakeRedis(decode_responses=True)) assert isinstance(store, imported.MemoryStore) + store.expire_track(999) def test_track_sequence_action_summary(): From 60d4cd9f702fcd3c0cb556c14179bbf071c450ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:50:17 +0000 Subject: [PATCH 6/8] Cover tracking lazy getattr in cv2 import regression test --- tests/test_memory.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_memory.py b/tests/test_memory.py index 6fe98c3..16bab42 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -70,6 +70,9 @@ def guarded_import(name, *args, **kwargs): store = imported.MemoryStore(redis_client=fakeredis.FakeRedis(decode_responses=True)) assert isinstance(store, imported.MemoryStore) store.expire_track(999) + tracking = importlib.import_module("services.tracking") + with pytest.raises(AttributeError): + getattr(tracking, "does_not_exist") def test_track_sequence_action_summary(): From 4d4337333ffca4332a93e74be2c04659c341f208 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:57:58 +0000 Subject: [PATCH 7/8] Add OpenCV to root requirements --- requirements.txt | Bin 40 -> 42 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index adade782662c9e691167dde70b7efb39db217e2a..d75b21da7500943573fe6dde0ed9b657a1c775d7 100644 GIT binary patch literal 42 xcmYdG%uX#zP01{_vo+E)GSD;N$}dRGOD@wbs4U6I&$F{N(X-Gq)HAo>0styd49Nfh literal 40 rcmezWFO4CQA)6tUp$Ldm7%~}(8SEHr8H^b8fY<;?8ZhuOa4`S?(r^b} From 02aa76ae72a2905a489f0aea33d0221fd904a6b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:08:03 +0000 Subject: [PATCH 8/8] fix: resolve phase3 test job lint failures in memory service --- services/memory/memory.py | 53 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/services/memory/memory.py b/services/memory/memory.py index e74fe76..f48e276 100644 --- a/services/memory/memory.py +++ b/services/memory/memory.py @@ -39,6 +39,7 @@ from libs.schemas.tracking import TrackLifecycleEvent, TrackState from libs.schemas.memory import TrackEvent, TrackSequence, ActionHint from libs.config.settings import settings +from services.memory.baseline import ZoneBaseline from services.tracking.cross_camera_reid import CrossCameraReID logger = logging.getLogger(__name__) @@ -214,7 +215,12 @@ def _load_record(self, camera_id: str, track_id: int) -> Optional[dict]: raw = self._r.get(self._track_key(camera_id, track_id)) return json.loads(raw) if raw else None - def _update_record(self, event: TrackLifecycleEvent, state: str) -> None: + def _update_record( + self, + event: TrackLifecycleEvent, + state: str, + anomalous: bool = False, + ) -> None: """ Update an existing track record's state and timing fields in Redis. @@ -308,17 +314,21 @@ def __init__(self, redis_client, camera_id: str = "cam_01") -> None: # ── Key helpers ─────────────────────────────────────────────────────────── - def _seq_key(self, track_id: int) -> str: - return f"seq:{self._camera_id}:{track_id}" + def _seq_key(self, track_id: int, camera_id: Optional[str] = None) -> str: + cam = camera_id or self._camera_id + return f"seq:{cam}:{track_id}" - def _zones_key(self, track_id: int) -> str: - return f"zones:{self._camera_id}:{track_id}" + def _zones_key(self, track_id: int, camera_id: Optional[str] = None) -> str: + cam = camera_id or self._camera_id + return f"zones:{cam}:{track_id}" - def _zone_count_key(self, track_id: int, zone: str) -> str: - return f"zone_count:{self._camera_id}:{track_id}:{zone}" + def _zone_count_key(self, track_id: int, zone: str, camera_id: Optional[str] = None) -> str: + cam = camera_id or self._camera_id + return f"zone_count:{cam}:{track_id}:{zone}" - def _active_key(self) -> str: - return f"active:{self._camera_id}" + def _active_key(self, camera_id: Optional[str] = None) -> str: + cam = camera_id or self._camera_id + return f"active:{cam}" def store_event(self, event) -> None: """ @@ -358,28 +368,23 @@ def get_sequence(self, track_id: int, last_n: Optional[int] = None, camera_id: O Returns: ``TrackSequence`` (empty if the track has no stored events). """ - from libs.schemas.memory import TrackEvent - - key = self._seq_key(track_id) + key = self._seq_key(track_id, camera_id) raw_list = self._r.lrange(key, -last_n, -1) if last_n else self._r.lrange(key, 0, -1) - - def get_active_track_ids(self, camera_id: str) -> set[int]: - members = self._r.smembers(self._active_key(camera_id)) - result: set[int] = set() - for m in members: + events: list[TrackEvent] = [] + for raw in raw_list: try: data = json.loads(raw if isinstance(raw, str) else raw.decode()) events.append(TrackEvent(**data)) except Exception: continue - zones_raw = self._r.smembers(self._zones_key(track_id)) + zones_raw = self._r.smembers(self._zones_key(track_id, camera_id)) zones_visited = [z if isinstance(z, str) else z.decode() for z in zones_raw] total_dwell = sum(e.dwell_time_seconds for e in events) return TrackSequence( track_id=track_id, - camera_id=self._camera_id, + camera_id=camera_id or self._camera_id, events=events, zones_visited=zones_visited, total_dwell=total_dwell, @@ -387,20 +392,20 @@ def get_active_track_ids(self, camera_id: str) -> set[int]: def get_zone_entry_count(self, track_id: int, zone: str, camera_id: Optional[str] = None) -> int: """Return the number of times *track_id* has entered *zone*.""" - raw = self._r.get(self._zone_count_key(track_id, zone)) + raw = self._r.get(self._zone_count_key(track_id, zone, camera_id)) if raw is None: return 0 return int(raw if isinstance(raw, (int, str)) else raw.decode()) def get_active_track_ids(self, camera_id: str) -> set[int]: """Return the set of track IDs currently marked active for *camera_id*.""" - members = self._r.smembers(f"active:{camera_id}") + members = self._r.smembers(self._active_key(camera_id)) return {int(m if isinstance(m, (int, str)) else m.decode()) for m in members} def expire_track(self, track_id: int, camera_id: Optional[str] = None) -> None: """Remove all stored data for *track_id* and deregister it as active.""" pipe = self._r.pipeline() - pipe.delete(self._seq_key(track_id)) - pipe.delete(self._zones_key(track_id)) - pipe.srem(self._active_key(), str(track_id)) + pipe.delete(self._seq_key(track_id, camera_id)) + pipe.delete(self._zones_key(track_id, camera_id)) + pipe.srem(self._active_key(camera_id), str(track_id)) pipe.execute()