Add production diagnostics and planning features
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-18 11:53:53 +02:00
parent d9dc186f9b
commit 575211f0db
12 changed files with 737 additions and 10 deletions

View File

@@ -354,6 +354,51 @@ def test_feedback_adapts_sensor_weights_and_model_can_rollback(tmp_path: Path) -
assert rollback.json()["behavior"]["active_model_version"] == version_id
def test_feedback_never_automate_sets_manual_block(tmp_path: Path) -> None:
with TestClient(app) as client:
_install_service(tmp_path)
client.post(
"/v1/actuators",
json={"actuator_entity_id": "light.abstellkammer"},
)
feedback = client.post(
"/v1/actuators/light.abstellkammer/feedback",
json={"correct": False, "kind": "never_automate"},
)
assert feedback.status_code == 200
payload = feedback.json()
assert payload["behavior"]["safety"]["manual_block"] is True
assert payload["behavior"]["feedback_log"][-1] == "never_automate"
def test_backup_export_restore_and_planning_refresh(tmp_path: Path) -> None:
with TestClient(app) as client:
_install_service(tmp_path)
client.post("/v1/actuators", json={"actuator_entity_id": "light.abstellkammer"})
backup = client.get("/v1/actuators/backup/export")
dry_run = client.post(
"/v1/actuators/light.abstellkammer/dry-run",
json={"enabled": True},
)
planning = client.post("/v1/actuators/planning/refresh")
restore = client.post(
"/v1/actuators/backup/restore",
json={"backup": backup.json(), "replace_existing": True},
)
assert backup.status_code == 200
assert backup.json()["records"][0]["actuator_entity_id"] == "light.abstellkammer"
assert dry_run.status_code == 200
assert dry_run.json()["behavior"]["dry_run_enabled"] is True
assert planning.status_code == 200
assert "agent_insights" in planning.json()[0]["behavior"]
assert restore.status_code == 200
assert restore.json()["restored_records"] == 1
def test_summary_is_lightweight_and_uses_cached_entity_metadata(tmp_path: Path) -> None:
with TestClient(app) as client:
_install_service(tmp_path)

View File

@@ -778,3 +778,129 @@ def test_state_change_uses_event_cache_without_rest_state_query(
assert reader.service_calls == [
("light", "turn_on", {"entity_id": "light.storage"})
]
def test_event_evaluation_records_decision_timeline_and_latency(
tmp_path: Path,
) -> None:
now = datetime.now(timezone.utc).replace(microsecond=0)
settings = _settings(tmp_path)
store = ActuatorStore(settings.actuator_store)
record = store.configure("light.storage")
record = record.model_copy(
update={
"assignment": record.assignment.model_copy(
update={"selected_context_entity_ids": ["binary_sensor.storage_door"]}
),
"behavior": record.behavior.model_copy(
update={
"mode": BehaviorMode.ACTIVE,
"status": BehaviorStatus.TRAINED,
"activation_ready": True,
"patterns": [
BehaviorPattern(
target_state="on",
minute_of_day=60,
weekday=0,
context_states={"binary_sensor.storage_door": "on"},
trigger_entity_id="binary_sensor.storage_door",
trigger_from_state="off",
trigger_to_state="on",
source="automation",
weight=1.0,
observed_at=now - timedelta(days=days_ago),
)
for days_ago in (3, 2, 1)
],
}
),
}
)
store.upsert(record)
reader = FakeBehaviorReader(
entities=[
HaEntitySummary(entity_id="light.storage", domain="light", state="off"),
HaEntitySummary(
entity_id="binary_sensor.storage_door",
domain="binary_sensor",
state="on",
last_changed=now,
),
],
history=[],
logbook=[],
)
engine = BehaviorEngine(ha_reader=reader, store=store, settings=settings)
result = engine.evaluate(
"light.storage",
trigger_entity_id="binary_sensor.storage_door",
trigger_state="on",
event_received_at=now,
)
trace = result.behavior.decision_timeline[-1]
latency = result.behavior.latency_measurements[-1]
assert trace.trigger_entity_id == "binary_sensor.storage_door"
assert trace.target_state == "on"
assert trace.executed is True
assert latency.trigger_entity_id == "binary_sensor.storage_door"
assert latency.executed is True
def test_dry_run_records_without_calling_service(tmp_path: Path) -> None:
now = datetime.now(timezone.utc).replace(microsecond=0)
settings = _settings(tmp_path)
store = ActuatorStore(settings.actuator_store)
record = store.configure("light.storage")
record = record.model_copy(
update={
"assignment": record.assignment.model_copy(
update={"selected_context_entity_ids": ["binary_sensor.storage_door"]}
),
"behavior": record.behavior.model_copy(
update={
"mode": BehaviorMode.ACTIVE,
"status": BehaviorStatus.TRAINED,
"activation_ready": True,
"dry_run_enabled": True,
"patterns": [
BehaviorPattern(
target_state="on",
minute_of_day=60,
weekday=0,
context_states={"binary_sensor.storage_door": "on"},
trigger_entity_id="binary_sensor.storage_door",
trigger_from_state="off",
trigger_to_state="on",
source="automation",
weight=1.0,
observed_at=now - timedelta(days=days_ago),
)
for days_ago in (3, 2, 1)
],
}
),
}
)
store.upsert(record)
reader = FakeBehaviorReader(
entities=[
HaEntitySummary(entity_id="light.storage", domain="light", state="off"),
HaEntitySummary(
entity_id="binary_sensor.storage_door",
domain="binary_sensor",
state="on",
last_changed=now,
),
],
history=[],
logbook=[],
)
engine = BehaviorEngine(ha_reader=reader, store=store, settings=settings)
result = engine.evaluate("light.storage")
assert reader.service_calls == []
assert result.behavior.dry_run_sample_count == 1
assert result.behavior.decision_timeline[-1].executed is False