feat(datastore): support --kind flag and add 15m duplicate suppression
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
@@ -21,6 +22,7 @@ def temp_datastore():
|
||||
if schema_src.is_dir():
|
||||
shutil.copytree(schema_src, data_dir / "_schema")
|
||||
|
||||
(data_dir / "_meta.json").write_text("{}")
|
||||
engine = DatastoreEngine(data_dir=data_dir)
|
||||
yield engine
|
||||
shutil.rmtree(temp_dir)
|
||||
@@ -384,3 +386,191 @@ def test_sleep_over_midnight_attribution(temp_datastore: DatastoreEngine):
|
||||
assert "sleep" not in thu_summary or thu_summary["sleep"] is None
|
||||
|
||||
|
||||
def test_duplicate_suppression_nutrition(temp_datastore: DatastoreEngine):
|
||||
"""Verifies that duplicate intake events within 15 minutes are suppressed idempotently."""
|
||||
start_time = "2026-10-09T08:00:00+02:00"
|
||||
payload = {
|
||||
"item": "Protein Porridge",
|
||||
"qty": 300,
|
||||
"unit": "g",
|
||||
"protein_g": 35.0,
|
||||
"kcal": 400.0,
|
||||
}
|
||||
|
||||
# 1. First logging
|
||||
res1 = temp_datastore.add_event(
|
||||
kind="nutrition.intake",
|
||||
subject="moritz",
|
||||
actor="agent:koch",
|
||||
start=start_time,
|
||||
payload=payload,
|
||||
)
|
||||
assert res1["status"] == "success"
|
||||
assert res1.get("duplicate") is not True
|
||||
first_id = res1["event"]["id"]
|
||||
|
||||
# 2. Duplicate logging 5 minutes later
|
||||
res2 = temp_datastore.add_event(
|
||||
kind="nutrition.intake",
|
||||
subject="moritz",
|
||||
actor="agent:koch",
|
||||
start="2026-10-09T08:05:00+02:00",
|
||||
payload=payload,
|
||||
)
|
||||
assert res2["status"] == "success"
|
||||
assert res2.get("duplicate") is True
|
||||
assert res2["event"]["id"] == first_id
|
||||
|
||||
# Verify DuckDB summary: only 1 item counted
|
||||
sum1 = temp_datastore.get_summary(subject="moritz", date_str="2026-10-09")
|
||||
assert sum1["nutrition"]["item_count"] == 1
|
||||
assert sum1["nutrition"]["total_kcal"] == 400.0
|
||||
|
||||
# 3. Explicit bypass with allow_duplicate=True
|
||||
res3 = temp_datastore.add_event(
|
||||
kind="nutrition.intake",
|
||||
subject="moritz",
|
||||
actor="agent:koch",
|
||||
start="2026-10-09T08:06:00+02:00",
|
||||
payload=payload,
|
||||
allow_duplicate=True,
|
||||
)
|
||||
assert res3["status"] == "success"
|
||||
assert res3.get("duplicate") is not True
|
||||
assert res3["event"]["id"] != first_id
|
||||
|
||||
# Verify DuckDB summary: 2 items counted
|
||||
sum2 = temp_datastore.get_summary(subject="moritz", date_str="2026-10-09")
|
||||
assert sum2["nutrition"]["item_count"] == 2
|
||||
assert sum2["nutrition"]["total_kcal"] == 800.0
|
||||
|
||||
# 4. Different item within 15 minutes is NOT suppressed
|
||||
res4 = temp_datastore.add_event(
|
||||
kind="nutrition.intake",
|
||||
subject="moritz",
|
||||
actor="agent:koch",
|
||||
start="2026-10-09T08:07:00+02:00",
|
||||
payload={"item": "Apfel", "qty": 150, "kcal": 80.0},
|
||||
)
|
||||
assert res4.get("duplicate") is not True
|
||||
|
||||
# 5. Same item outside 15 minutes window is NOT suppressed
|
||||
res5 = temp_datastore.add_event(
|
||||
kind="nutrition.intake",
|
||||
subject="moritz",
|
||||
actor="agent:koch",
|
||||
start="2026-10-09T09:30:00+02:00",
|
||||
payload=payload,
|
||||
)
|
||||
assert res5.get("duplicate") is not True
|
||||
|
||||
|
||||
def test_duplicate_suppression_sleep(temp_datastore: DatastoreEngine):
|
||||
"""Verifies that starting the same sleep session within 15 minutes returns existing open session."""
|
||||
res1 = temp_datastore.add_event(
|
||||
kind="sleep.session",
|
||||
subject="mila",
|
||||
actor="agent:hebamme",
|
||||
start="2026-10-09T13:00:00+02:00",
|
||||
payload={"type": "nap", "location": "bett"},
|
||||
)
|
||||
assert res1["status"] == "success"
|
||||
first_id = res1["event"]["id"]
|
||||
|
||||
# Repeat 3 minutes later: should return existing open session without throwing ValueError
|
||||
res2 = temp_datastore.add_event(
|
||||
kind="sleep.session",
|
||||
subject="mila",
|
||||
actor="agent:hebamme",
|
||||
start="2026-10-09T13:03:00+02:00",
|
||||
payload={"type": "nap", "location": "bett"},
|
||||
)
|
||||
assert res2["status"] == "success"
|
||||
assert res2.get("duplicate") is True
|
||||
assert res2["event"]["id"] == first_id
|
||||
|
||||
# Stop session
|
||||
temp_datastore.stop_event(
|
||||
kind="sleep.session",
|
||||
subject="mila",
|
||||
end_time="2026-10-09T14:00:00+02:00",
|
||||
)
|
||||
|
||||
|
||||
def test_cli_kind_flag_and_positional(temp_datastore: DatastoreEngine):
|
||||
"""Verifies CLI accepts both positional and --kind flag for add and stop commands."""
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
script_path = Path(__file__).resolve().parent.parent / "scripts" / "datastore.py"
|
||||
data_dir = str(temp_datastore.data_dir)
|
||||
|
||||
# 1. Add using --kind flag
|
||||
proc_add = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"add",
|
||||
"--kind",
|
||||
"nutrition.intake",
|
||||
"--subject",
|
||||
"moritz",
|
||||
"--payload",
|
||||
'{"item": "CLI Snack", "qty": 50, "kcal": 100}',
|
||||
"--start",
|
||||
"2026-10-09T15:00:00+02:00",
|
||||
],
|
||||
env=dict(os.environ, OPENCLAW_DATA_DIR=data_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert proc_add.returncode == 0
|
||||
data = json.loads(proc_add.stdout)
|
||||
assert data["status"] == "success"
|
||||
assert data["event"]["kind"] == "nutrition.intake"
|
||||
|
||||
# 2. Add open sleep using positional kind
|
||||
proc_sleep_start = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"add",
|
||||
"sleep.session",
|
||||
"--subject",
|
||||
"mila",
|
||||
"--payload",
|
||||
'{"type": "nap"}',
|
||||
"--start",
|
||||
"2026-10-09T15:00:00+02:00",
|
||||
],
|
||||
env=dict(os.environ, OPENCLAW_DATA_DIR=data_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert proc_sleep_start.returncode == 0
|
||||
|
||||
# 3. Stop sleep using --kind flag
|
||||
proc_sleep_stop = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"stop",
|
||||
"--kind",
|
||||
"sleep.session",
|
||||
"--subject",
|
||||
"mila",
|
||||
"--end",
|
||||
"2026-10-09T16:00:00+02:00",
|
||||
],
|
||||
env=dict(os.environ, OPENCLAW_DATA_DIR=data_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert proc_sleep_stop.returncode == 0
|
||||
stop_data = json.loads(proc_sleep_stop.stdout)
|
||||
assert stop_data["status"] == "success"
|
||||
assert stop_data["duration_minutes"] == 60
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user