Each ml/features/*.py now declares freshness, source, and fallback per feature. ProfileFeature gains ttl_sec (mirrored from registry.ts), freshness="batched", source, and fallback. context.py adds ContextFeatureSpec + CONTEXT_FEATURES for the three JIT features (hour_of_day, day_of_week, tasks). CI test parses ttlSec from registry.ts to catch drift. ml/README updated with split JIT/batched feature contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
"""Tests for ml/features/context.py"""
|
|
import pytest
|
|
import sys, os; sys.path.insert(0, os.path.dirname(__file__))
|
|
from context import build_context, TaskSignal, PromptContext, CONTEXT_FEATURES
|
|
|
|
|
|
def test_empty_tasks():
|
|
ctx = build_context([], hour_of_day=9, day_of_week=1)
|
|
assert ctx.tasks == []
|
|
assert ctx.hour_of_day == 9
|
|
assert ctx.day_of_week == 1
|
|
|
|
|
|
def test_overdue_tasks_sorted_first():
|
|
tasks = [
|
|
TaskSignal(id="a", content="Normal task", priority=1, is_overdue=False),
|
|
TaskSignal(id="b", content="Overdue task", priority=2, is_overdue=True, task_age_days=3.0),
|
|
]
|
|
ctx = build_context(tasks)
|
|
assert ctx.tasks[0]["id"] == "b"
|
|
|
|
|
|
def test_high_priority_within_non_overdue():
|
|
tasks = [
|
|
TaskSignal(id="lo", content="Low prio", priority=1, is_overdue=False),
|
|
TaskSignal(id="hi", content="High prio", priority=4, is_overdue=False),
|
|
]
|
|
ctx = build_context(tasks)
|
|
assert ctx.tasks[0]["id"] == "hi"
|
|
|
|
|
|
def test_extra_fields_passed_through():
|
|
ctx = build_context([], extra={"mood": "focused"})
|
|
assert ctx.extra["mood"] == "focused"
|
|
|
|
|
|
def test_task_age_rounded():
|
|
tasks = [TaskSignal(id="x", content="Task", task_age_days=1.23456)]
|
|
ctx = build_context(tasks)
|
|
assert ctx.tasks[0]["task_age_days"] == 1.2
|
|
|
|
|
|
def test_overdue_sorted_by_priority():
|
|
tasks = [
|
|
TaskSignal(id="lo", content="Low", priority=1, is_overdue=True),
|
|
TaskSignal(id="hi", content="High", priority=4, is_overdue=True),
|
|
]
|
|
ctx = build_context(tasks)
|
|
assert ctx.tasks[0]["id"] == "hi"
|
|
|
|
|
|
def test_overdue_same_priority_sorted_by_age():
|
|
tasks = [
|
|
TaskSignal(id="new", content="New", priority=2, is_overdue=True, task_age_days=1.0),
|
|
TaskSignal(id="old", content="Old", priority=2, is_overdue=True, task_age_days=5.0),
|
|
]
|
|
ctx = build_context(tasks)
|
|
assert ctx.tasks[0]["id"] == "old"
|
|
|
|
|
|
def test_due_date_none_preserved():
|
|
tasks = [TaskSignal(id="x", content="No due", due_date=None)]
|
|
ctx = build_context(tasks)
|
|
assert ctx.tasks[0]["due_date"] is None
|
|
|
|
|
|
# ── CONTEXT_FEATURES spec tests (issue #61) ──────────────────────────────────
|
|
|
|
def test_context_features_expected_names():
|
|
names = {f.name for f in CONTEXT_FEATURES}
|
|
assert names == {"hour_of_day", "day_of_week", "tasks"}
|
|
|
|
|
|
def test_context_features_all_jit():
|
|
for f in CONTEXT_FEATURES:
|
|
assert f.freshness == "jit", f"{f.name}: expected freshness='jit', got {f.freshness!r}"
|
|
|
|
|
|
def test_context_features_source_set():
|
|
for f in CONTEXT_FEATURES:
|
|
assert f.source, f"{f.name}: source must not be empty"
|
|
|
|
|
|
def test_context_features_fallback_set():
|
|
for f in CONTEXT_FEATURES:
|
|
assert f.fallback, f"{f.name}: fallback must not be empty"
|
|
|
|
|
|
def test_context_features_no_duplicates():
|
|
names = [f.name for f in CONTEXT_FEATURES]
|
|
assert len(names) == len(set(names)), f"duplicate names: {names}"
|