feat: add disposal evidence engine handling
This commit is contained in:
34
progress.md
34
progress.md
@@ -193,13 +193,47 @@
|
||||
| 2026-05-29 | Setup | Main Agent | Created Goal for `v1.2 轨迹识别` using `/Users/yoilun/Code/cold_display_guard` as the real project path | Active goal tracks lightweight trajectory detection plus YOLO-ready evidence contract |
|
||||
| 2026-05-29 | Setup | Main Agent | Read `/Users/yoilun/Code/goal-subagents-workflow-prompt.md` | Workflow requires task files, stage-based coding/testing agents, bug loop limit, and standard subagent context header |
|
||||
| 2026-05-29 | Setup | Main Agent | Read existing `task_plan.md`, `findings.md`, `progress.md`, and `docs/project.md` | v1.1 state is complete; v1.2 plan can start on branch `lightweight-trajectory-tracking` |
|
||||
| 2026-05-29 | Phase 1 | Main Agent | Marked Phase 1 as `in_progress` | Preparing coding agent for data contract and engine evidence handling |
|
||||
| 2026-05-29 | Phase 1 | Coding Agent | Implemented initial `disposal_evidence` contract and engine handling | Target engine tests and full Python tests passed in coding agent run, but testing agent found review issues |
|
||||
| 2026-05-29 | Phase 1 | Testing Agent | Reviewed phase 1 implementation | Verdict fail: evidence/count double consumption, missing target validation, null classifier fields coercion |
|
||||
| 2026-05-29 | Phase 1 | Coding Agent | Fixed testing-agent findings | Added target validation, nullable optional fields, and evidence/count double-consume guard |
|
||||
| 2026-05-29 | Phase 1 | Testing Agent | Re-tested phase 1 fixes | Verdict pass; no bugs found |
|
||||
| 2026-05-29 | Phase 1 | Main Agent | Ran local verification | `tests.test_engine` passed with 24 tests; full Python suite passed with 55 tests |
|
||||
|
||||
### Test Results
|
||||
|
||||
| Time | Command | Result | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_engine -v` | pass | 24 engine tests passed after phase 1 evidence fixes |
|
||||
| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 55 full Python tests passed after phase 1 |
|
||||
|
||||
### Bug Loop
|
||||
|
||||
| Phase | Bug | Fix Attempt | Retest Result |
|
||||
| --- | --- | --- | --- |
|
||||
| Phase 1 | `disposal_evidence` and `trash_deposit_count` can double-consume the same disposal signal | Added regression test and suppress generic trash fallback when confirming source-specific evidence exists in the observation | Resolved; testing agent and local full Python suite passed |
|
||||
| Phase 1 | High-confidence evidence with non-trash target can close pending disposal | Added target whitelist for `trash` / `trash_bin` plus regression test | Resolved; testing agent and local full Python suite passed |
|
||||
| Phase 1 | `item_class: null` and `detector_score: null` lose null semantics | Changed optional evidence fields to preserve `None` plus regression test | Resolved; testing agent and local full Python suite passed |
|
||||
|
||||
## 2026-05-29 Phase Completed: Phase 1 - Data Contract And Engine Evidence Handling
|
||||
|
||||
Status: complete
|
||||
|
||||
Files Changed:
|
||||
- `src/cold_display_guard/models.py`
|
||||
- `src/cold_display_guard/engine.py`
|
||||
- `tests/test_engine.py`
|
||||
- `task_plan.md`
|
||||
- `progress.md`
|
||||
|
||||
Tests:
|
||||
- `PYTHONPATH=src python3 -m unittest tests.test_engine -v`: pass
|
||||
- `PYTHONPATH=src python3 -m unittest discover -s tests -v`: pass
|
||||
|
||||
Notes:
|
||||
- `Observation` now supports `disposal_evidence`.
|
||||
- `BatchEngine` applies source-zone evidence before generic trash fallback and again after same-frame zone transitions.
|
||||
- Evidence must meet confidence threshold and target the trash.
|
||||
|
||||
Risks:
|
||||
- Threshold is currently a phase-1 constant; later runtime config integration will make it configurable.
|
||||
|
||||
@@ -3,7 +3,11 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from cold_display_guard.models import Batch, EngineSettings, Observation
|
||||
from cold_display_guard.models import Batch, DisposalEvidence, EngineSettings, Observation
|
||||
|
||||
|
||||
DISPOSAL_EVIDENCE_CONFIDENCE_THRESHOLD = 0.72
|
||||
TRASH_DISPOSAL_TARGETS = {"trash", "trash_bin"}
|
||||
|
||||
|
||||
class BatchEngine:
|
||||
@@ -20,8 +24,19 @@ class BatchEngine:
|
||||
zone_counts = self._normalized_counts(observation.zone_counts)
|
||||
previous_zone_counts = dict(self._zone_counts)
|
||||
remaining_trash_deposits = observation.trash_deposit_count
|
||||
used_disposal_evidence: set[int] = set()
|
||||
has_source_specific_disposal = self._has_confirming_disposal_evidence(observation.disposal_evidence)
|
||||
if has_source_specific_disposal:
|
||||
remaining_trash_deposits = 0
|
||||
|
||||
events.extend(self._expire_pending_disposal(observation.ts))
|
||||
events.extend(
|
||||
self._apply_disposal_evidence(
|
||||
observation.ts,
|
||||
observation.disposal_evidence,
|
||||
used_disposal_evidence,
|
||||
)
|
||||
)
|
||||
trash_events = self._apply_trash_deposits(observation.ts, remaining_trash_deposits)
|
||||
remaining_trash_deposits = max(0, remaining_trash_deposits - len(trash_events))
|
||||
events.extend(trash_events)
|
||||
@@ -67,6 +82,13 @@ class BatchEngine:
|
||||
self._zone_counts[zone_id] = new_count
|
||||
|
||||
newly_pending_count = max(0, len(self.pending_disposal) - pending_count_before_zone_transitions)
|
||||
events.extend(
|
||||
self._apply_disposal_evidence(
|
||||
observation.ts,
|
||||
observation.disposal_evidence,
|
||||
used_disposal_evidence,
|
||||
)
|
||||
)
|
||||
trash_deposits_to_apply = remaining_trash_deposits
|
||||
if remaining_trash_deposits > 0 and newly_pending_count > 1:
|
||||
trash_deposits_to_apply = max(remaining_trash_deposits, newly_pending_count)
|
||||
@@ -239,6 +261,42 @@ class BatchEngine:
|
||||
deposit_count -= 1
|
||||
return events
|
||||
|
||||
def _apply_disposal_evidence(
|
||||
self,
|
||||
when: datetime,
|
||||
disposal_evidence: list[DisposalEvidence],
|
||||
used_evidence_indices: set[int],
|
||||
) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
for index, evidence in enumerate(disposal_evidence):
|
||||
if index in used_evidence_indices:
|
||||
continue
|
||||
if not self._is_confirming_disposal_evidence(evidence):
|
||||
continue
|
||||
pending_index = self._pending_index_for_source_zone(evidence.source_zone_id)
|
||||
if pending_index is None:
|
||||
continue
|
||||
batch = self.pending_disposal.pop(pending_index)
|
||||
batch.state = "discarded"
|
||||
self.closed_batches.append(batch)
|
||||
used_evidence_indices.add(index)
|
||||
events.append(self._event("batch_discarded", when, batch, severity="info"))
|
||||
return events
|
||||
|
||||
def _has_confirming_disposal_evidence(self, disposal_evidence: list[DisposalEvidence]) -> bool:
|
||||
return any(self._is_confirming_disposal_evidence(evidence) for evidence in disposal_evidence)
|
||||
|
||||
def _is_confirming_disposal_evidence(self, evidence: DisposalEvidence) -> bool:
|
||||
if evidence.confidence < DISPOSAL_EVIDENCE_CONFIDENCE_THRESHOLD:
|
||||
return False
|
||||
return evidence.target.lower() in TRASH_DISPOSAL_TARGETS
|
||||
|
||||
def _pending_index_for_source_zone(self, source_zone_id: str) -> int | None:
|
||||
for index, batch in enumerate(self.pending_disposal):
|
||||
if batch.zone_id == source_zone_id:
|
||||
return index
|
||||
return None
|
||||
|
||||
def _expire_pending_disposal(self, when: datetime) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
still_pending: list[Batch] = []
|
||||
|
||||
@@ -24,11 +24,39 @@ class EngineSettings:
|
||||
return timedelta(seconds=self.trash_confirmation_seconds)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DisposalEvidence:
|
||||
source_zone_id: str
|
||||
target: str
|
||||
confidence: float
|
||||
method: str
|
||||
track_points: list[Any]
|
||||
item_class: str | None
|
||||
detector_score: float | None
|
||||
observed_at: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> "DisposalEvidence":
|
||||
return cls(
|
||||
source_zone_id=str(payload.get("source_zone_id", "")).strip(),
|
||||
target=str(payload.get("target", "")).strip(),
|
||||
confidence=_float_or_zero(payload.get("confidence", 0.0)),
|
||||
method=str(payload.get("method", "")).strip(),
|
||||
track_points=_normalize_track_points(payload.get("track_points", [])),
|
||||
item_class=_optional_string(payload.get("item_class")),
|
||||
detector_score=_optional_float(payload.get("detector_score")),
|
||||
observed_at=_optional_string(
|
||||
payload.get("observed_at", payload.get("detected_at", payload.get("ts")))
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Observation:
|
||||
ts: datetime
|
||||
zone_counts: dict[str, int]
|
||||
trash_deposit_count: int = 0
|
||||
disposal_evidence: list[DisposalEvidence] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> "Observation":
|
||||
@@ -46,6 +74,7 @@ class Observation:
|
||||
ts=ts,
|
||||
zone_counts={key: max(0, int(value)) for key, value in payload["zone_counts"].items()},
|
||||
trash_deposit_count=max(0, trash_deposit_count),
|
||||
disposal_evidence=_normalize_disposal_evidence(payload.get("disposal_evidence", [])),
|
||||
)
|
||||
|
||||
|
||||
@@ -67,3 +96,47 @@ class Batch:
|
||||
if self.ended_at is not None:
|
||||
return self.dwell_seconds
|
||||
return max(0, int((when - self.started_at).total_seconds()))
|
||||
|
||||
|
||||
def _normalize_disposal_evidence(raw_evidence: Any) -> list[DisposalEvidence]:
|
||||
if raw_evidence is None:
|
||||
return []
|
||||
if isinstance(raw_evidence, dict):
|
||||
raw_items = [raw_evidence]
|
||||
else:
|
||||
raw_items = raw_evidence
|
||||
return [
|
||||
DisposalEvidence.from_dict(item)
|
||||
for item in raw_items
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
|
||||
|
||||
def _normalize_track_points(raw_track_points: Any) -> list[Any]:
|
||||
if isinstance(raw_track_points, list):
|
||||
return list(raw_track_points)
|
||||
if isinstance(raw_track_points, tuple):
|
||||
return list(raw_track_points)
|
||||
return []
|
||||
|
||||
|
||||
def _float_or_zero(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _optional_float(value: Any) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _optional_string(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return str(value).strip()
|
||||
|
||||
@@ -96,7 +96,7 @@ Create and evolve an independent git project under `~/Code` for monitoring food
|
||||
|
||||
| Phase | Status | Goal | Acceptance Criteria |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | pending | 建立 `disposal_evidence` 数据契约并让状态机优先按来源区域丢弃 | `Observation` 支持 evidence;engine 能按 `source_zone_id` 精确关闭 pending batch;同帧移除+evidence 有回归测试;旧 `trash_deposit_count` 仍可兜底 |
|
||||
| 1 | complete | 建立 `disposal_evidence` 数据契约并让状态机优先按来源区域丢弃 | `Observation` 支持 evidence;engine 能按 `source_zone_id` 精确关闭 pending batch;同帧移除+evidence 有回归测试;旧 `trash_deposit_count` 仍可兜底 |
|
||||
| 2 | pending | 实现无 YOLO 依赖的轻量轨迹检测 | synthetic frame 测试覆盖源区域到垃圾桶、非源区域运动、未到垃圾桶、单帧反光、多候选互不串扰;不引入模型依赖 |
|
||||
| 3 | pending | 集成 runtime 配置、诊断和候选窗口加速采样 | `main.py` 写入 `disposal_evidence` 与 trajectory diagnostics;配置默认 `trajectory_enabled=true`、`yolo_enabled=false`;候选活跃时使用更短采样间隔 |
|
||||
| 4 | pending | 文档、全量验证和部署准备 | README/project/progress 更新;Python 全量测试通过;前端测试/构建按影响范围验证;远端部署命令和风险记录清楚 |
|
||||
|
||||
@@ -9,16 +9,39 @@ from cold_display_guard import BatchEngine, EngineSettings, Observation
|
||||
UTC = timezone.utc
|
||||
|
||||
|
||||
def obs(ts: datetime, counts: dict[str, int], trash: bool | int = False) -> Observation:
|
||||
def obs(
|
||||
ts: datetime,
|
||||
counts: dict[str, int],
|
||||
trash: bool | int = False,
|
||||
disposal_evidence: list[dict[str, object]] | None = None,
|
||||
) -> Observation:
|
||||
return Observation.from_dict(
|
||||
{
|
||||
"ts": ts.isoformat(),
|
||||
"zone_counts": counts,
|
||||
"trash_deposit": trash,
|
||||
"disposal_evidence": disposal_evidence or [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def disposal_evidence(
|
||||
source_zone_id: str,
|
||||
confidence: float = 0.93,
|
||||
target: str = "trash_bin",
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"source_zone_id": source_zone_id,
|
||||
"target": target,
|
||||
"confidence": confidence,
|
||||
"method": "trajectory",
|
||||
"track_points": [{"x": 101, "y": 202, "ts": "2026-04-27T10:20:01+00:00"}],
|
||||
"item_class": "prepared_food",
|
||||
"detector_score": 0.88,
|
||||
"observed_at": "2026-04-27T10:20:02+00:00",
|
||||
}
|
||||
|
||||
|
||||
class BatchEngineTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.settings = EngineSettings(
|
||||
@@ -80,6 +103,200 @@ class BatchEngineTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual([event["event"] for event in events], ["batch_discarded"])
|
||||
|
||||
def test_observation_from_dict_normalizes_disposal_evidence(self) -> None:
|
||||
observation = Observation.from_dict(
|
||||
{
|
||||
"ts": self.t0.isoformat(),
|
||||
"zone_counts": {"1": "2"},
|
||||
"disposal_evidence": [
|
||||
{
|
||||
"source_zone_id": 1,
|
||||
"target": "trash_bin",
|
||||
"confidence": "0.83",
|
||||
"method": "trajectory",
|
||||
"track_points": [{"x": 1, "y": 2}],
|
||||
"item_class": "prepared_food",
|
||||
"detector_score": "0.91",
|
||||
"observed_at": "2026-04-27T10:00:01+00:00",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
evidence = observation.disposal_evidence[0]
|
||||
self.assertEqual(evidence.source_zone_id, "1")
|
||||
self.assertEqual(evidence.target, "trash_bin")
|
||||
self.assertAlmostEqual(evidence.confidence, 0.83)
|
||||
self.assertEqual(evidence.method, "trajectory")
|
||||
self.assertEqual(evidence.track_points, [{"x": 1, "y": 2}])
|
||||
self.assertEqual(evidence.item_class, "prepared_food")
|
||||
self.assertAlmostEqual(evidence.detector_score, 0.91)
|
||||
self.assertEqual(evidence.observed_at, "2026-04-27T10:00:01+00:00")
|
||||
|
||||
def test_observation_from_dict_preserves_null_optional_disposal_fields(self) -> None:
|
||||
observation = Observation.from_dict(
|
||||
{
|
||||
"ts": self.t0.isoformat(),
|
||||
"zone_counts": {"1": 1},
|
||||
"disposal_evidence": [
|
||||
{
|
||||
"source_zone_id": "1",
|
||||
"target": "trash_bin",
|
||||
"confidence": 0.83,
|
||||
"method": "trajectory",
|
||||
"track_points": [],
|
||||
"item_class": None,
|
||||
"detector_score": None,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
evidence = observation.disposal_evidence[0]
|
||||
self.assertIsNone(evidence.item_class)
|
||||
self.assertIsNone(evidence.detector_score)
|
||||
|
||||
def test_matching_disposal_evidence_discards_pending_batch(self) -> None:
|
||||
settings = EngineSettings(
|
||||
camera_id="test_cam",
|
||||
max_dwell_seconds=1200,
|
||||
trash_confirmation_seconds=120,
|
||||
zone_ids=("1",),
|
||||
)
|
||||
engine = BatchEngine(settings)
|
||||
engine.process(obs(self.t0, {"1": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1300), {"1": 0}))
|
||||
|
||||
events = engine.process(
|
||||
obs(
|
||||
self.t0 + timedelta(seconds=1310),
|
||||
{"1": 0},
|
||||
disposal_evidence=[disposal_evidence("1")],
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual([event["event"] for event in events], ["batch_discarded"])
|
||||
self.assertEqual(events[0]["zone_id"], "1")
|
||||
self.assertEqual(engine.pending_disposal, [])
|
||||
|
||||
def test_disposal_evidence_for_another_zone_does_not_discard_wrong_pending_batch(self) -> None:
|
||||
settings = EngineSettings(
|
||||
camera_id="test_cam",
|
||||
max_dwell_seconds=1200,
|
||||
trash_confirmation_seconds=120,
|
||||
zone_ids=("1", "4"),
|
||||
)
|
||||
engine = BatchEngine(settings)
|
||||
engine.process(obs(self.t0, {"1": 1, "4": 0}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1, "4": 0}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1300), {"1": 0, "4": 0}))
|
||||
|
||||
events = engine.process(
|
||||
obs(
|
||||
self.t0 + timedelta(seconds=1310),
|
||||
{"1": 0, "4": 0},
|
||||
disposal_evidence=[disposal_evidence("4")],
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(events, [])
|
||||
self.assertEqual([batch.zone_id for batch in engine.pending_disposal], ["1"])
|
||||
|
||||
def test_non_trash_disposal_evidence_target_is_ignored(self) -> None:
|
||||
settings = EngineSettings(
|
||||
camera_id="test_cam",
|
||||
max_dwell_seconds=1200,
|
||||
trash_confirmation_seconds=120,
|
||||
zone_ids=("1",),
|
||||
)
|
||||
engine = BatchEngine(settings)
|
||||
engine.process(obs(self.t0, {"1": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1300), {"1": 0}))
|
||||
|
||||
events = engine.process(
|
||||
obs(
|
||||
self.t0 + timedelta(seconds=1310),
|
||||
{"1": 0},
|
||||
disposal_evidence=[disposal_evidence("1", target="customer_hand")],
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(events, [])
|
||||
self.assertEqual([batch.zone_id for batch in engine.pending_disposal], ["1"])
|
||||
|
||||
def test_disposal_evidence_and_trash_count_do_not_double_consume_same_signal(self) -> None:
|
||||
settings = EngineSettings(
|
||||
camera_id="test_cam",
|
||||
max_dwell_seconds=1200,
|
||||
trash_confirmation_seconds=120,
|
||||
zone_ids=("1", "4"),
|
||||
)
|
||||
engine = BatchEngine(settings)
|
||||
engine.process(obs(self.t0, {"1": 1, "4": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1, "4": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1300), {"1": 0, "4": 0}))
|
||||
|
||||
events = engine.process(
|
||||
obs(
|
||||
self.t0 + timedelta(seconds=1310),
|
||||
{"1": 0, "4": 0},
|
||||
trash=True,
|
||||
disposal_evidence=[disposal_evidence("4")],
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual([(event["event"], event["zone_id"]) for event in events], [("batch_discarded", "4")])
|
||||
self.assertEqual([batch.zone_id for batch in engine.pending_disposal], ["1"])
|
||||
|
||||
def test_same_observation_removal_and_disposal_evidence_discards_newly_pending_batch(self) -> None:
|
||||
settings = EngineSettings(
|
||||
camera_id="test_cam",
|
||||
max_dwell_seconds=1200,
|
||||
trash_confirmation_seconds=120,
|
||||
zone_ids=("1",),
|
||||
)
|
||||
engine = BatchEngine(settings)
|
||||
engine.process(obs(self.t0, {"1": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1}))
|
||||
|
||||
events = engine.process(
|
||||
obs(
|
||||
self.t0 + timedelta(seconds=1300),
|
||||
{"1": 0},
|
||||
disposal_evidence=[disposal_evidence("1")],
|
||||
)
|
||||
)
|
||||
later_events = engine.process(obs(self.t0 + timedelta(seconds=1421), {"1": 0}))
|
||||
|
||||
self.assertEqual([event["event"] for event in events], ["batch_pending_disposal", "batch_discarded"])
|
||||
self.assertEqual(events[1]["zone_id"], "1")
|
||||
self.assertEqual(later_events, [])
|
||||
|
||||
def test_low_confidence_disposal_evidence_is_ignored(self) -> None:
|
||||
settings = EngineSettings(
|
||||
camera_id="test_cam",
|
||||
max_dwell_seconds=1200,
|
||||
trash_confirmation_seconds=120,
|
||||
zone_ids=("1",),
|
||||
)
|
||||
engine = BatchEngine(settings)
|
||||
engine.process(obs(self.t0, {"1": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1}))
|
||||
engine.process(obs(self.t0 + timedelta(seconds=1300), {"1": 0}))
|
||||
|
||||
events = engine.process(
|
||||
obs(
|
||||
self.t0 + timedelta(seconds=1310),
|
||||
{"1": 0},
|
||||
disposal_evidence=[disposal_evidence("1", confidence=0.71)],
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(events, [])
|
||||
self.assertEqual([batch.zone_id for batch in engine.pending_disposal], ["1"])
|
||||
|
||||
def test_missing_trash_deposit_escalates_warning_after_deadline(self) -> None:
|
||||
self.engine.process(obs(self.t0, {"r1c1": 2}))
|
||||
self.engine.process(obs(self.t0 + timedelta(seconds=11), {"r1c1": 0}))
|
||||
|
||||
Reference in New Issue
Block a user