Plant registry, grow event calendar, and gardening knowledge base populated via cf-harvest ingest endpoint. All 23 tests passing. Stack: - FastAPI + SQLite (cf-core migrations) on port 8522 - Vue 3 + Vite + Pinia SPA on port 8521 - 5 SQL migrations: locations → plants → grow_events, knowledge_sources → knowledge_facts - Earthy green theme (--color-primary: #4a7c59) matching CF theme system Knowledge ingest contract: - POST /api/v1/knowledge/ingest — idempotent on fact_hash (sha256 of type+payload+video_path) - 8 fact types: companion_planting, soil_amendment, propagation, pest_diagnosis, harvest_timing, instruction, medicinal, history_context - Partial batch failure: malformed facts collected in rejected[], valid facts inserted - Re-ingesting same video batch: facts_skipped_duplicate > 0, no duplicates Wired views: RegistryView (plant CRUD), KnowledgeView (facts + type filter chips) Stub views: CalendarView, SettingsView
144 lines
5.3 KiB
Python
144 lines
5.3 KiB
Python
"""cf-harvest knowledge ingest tests — critical path.
|
|
|
|
Covers: all 8 fact types, idempotent re-ingest, partial batch failure,
|
|
subject extraction, and fact_hash deduplication.
|
|
"""
|
|
import pytest
|
|
|
|
|
|
_SOURCE = {
|
|
"video_path": "/media/GardenersWorld/S59E01.mkv",
|
|
"presenter": "Monty Don",
|
|
"location": "Longmeadow",
|
|
"source_domain": "gardening",
|
|
}
|
|
|
|
_ALL_FACT_TYPES = [
|
|
("companion_planting", {
|
|
"plants": ["carrot", "spring onion"],
|
|
"relationship": "beneficial",
|
|
"benefit": "deters carrot root fly",
|
|
"spacing": "alternate rows",
|
|
"notes": None,
|
|
}),
|
|
("soil_amendment", {
|
|
"amendment": "pine bark mulch",
|
|
"purpose": "suppress weeds, retain moisture",
|
|
"application_rate": "5cm layer",
|
|
"timing": "early spring",
|
|
"target_plants": ["all beds"],
|
|
}),
|
|
("propagation", {
|
|
"plant": "raspberry",
|
|
"method": "cane",
|
|
"timing": "autumn",
|
|
"steps": ["cut cane to 30cm", "push into prepared soil", "firm in"],
|
|
"success_indicators": ["new leaves at base"],
|
|
"notes": None,
|
|
}),
|
|
("pest_diagnosis", {
|
|
"pest_or_disease": "couch grass",
|
|
"affected_plants": ["border perennials"],
|
|
"symptoms": ["white rhizomes through roots", "vigorous re-sprouting"],
|
|
"treatment": "fork out every rhizome fragment",
|
|
"prevention": "mulch to reduce light",
|
|
"organic": True,
|
|
}),
|
|
("harvest_timing", {
|
|
"plant": "raspberry",
|
|
"indicators": ["deep red colour", "comes free with gentle pull"],
|
|
"harvest_method": "gentle pull",
|
|
"post_harvest": "refrigerate within 24h",
|
|
}),
|
|
("instruction", {
|
|
"action": "mulch",
|
|
"subject": "border",
|
|
"steps": ["clear weeds", "apply 5cm pine bark"],
|
|
"timing": "early spring",
|
|
"notes": None,
|
|
}),
|
|
("medicinal", {
|
|
"plant": "Calendula officinalis",
|
|
"use": "wound healing, anti-inflammatory skin salve",
|
|
"preparation": "infused oil",
|
|
"cautions": "avoid during pregnancy",
|
|
}),
|
|
("history_context", {
|
|
"subject": "Narcissus triandrus",
|
|
"origin": "Iberian Peninsula",
|
|
"period": None,
|
|
"summary": "Angel's Tears — a small, nodding daffodil native to rocky hillsides.",
|
|
}),
|
|
]
|
|
|
|
|
|
def test_ingest_all_eight_fact_types(client):
|
|
facts = [{"fact_type": ft, "confidence": 0.9, "payload": p} for ft, p in _ALL_FACT_TYPES]
|
|
resp = client.post("/api/v1/knowledge/ingest", json={"source": _SOURCE, "facts": facts})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["facts_received"] == 8
|
|
assert data["facts_inserted"] == 8
|
|
assert data["facts_skipped_duplicate"] == 0
|
|
assert data["rejected"] == []
|
|
|
|
|
|
def test_ingest_idempotent(client):
|
|
facts = [{"fact_type": "instruction", "confidence": 0.8, "payload": {
|
|
"action": "water", "subject": "tomatoes", "steps": [], "timing": "morning",
|
|
}}]
|
|
payload = {"source": _SOURCE, "facts": facts}
|
|
|
|
first = client.post("/api/v1/knowledge/ingest", json=payload).json()
|
|
assert first["facts_inserted"] == 1
|
|
|
|
second = client.post("/api/v1/knowledge/ingest", json=payload).json()
|
|
assert second["facts_inserted"] == 0
|
|
assert second["facts_skipped_duplicate"] == 1
|
|
|
|
|
|
def test_ingest_partial_batch_continues_on_bad_fact(client):
|
|
facts = [
|
|
{"fact_type": "instruction", "confidence": 0.9, "payload": {
|
|
"action": "prune", "subject": "roses", "steps": [], "timing": "spring",
|
|
}},
|
|
{"fact_type": "unknown_type", "confidence": 0.5, "payload": {}},
|
|
{"fact_type": "medicinal", "confidence": 0.7, "payload": {
|
|
"plant": "Lavender", "use": "calming", "preparation": "tea", "cautions": None,
|
|
}},
|
|
]
|
|
resp = client.post("/api/v1/knowledge/ingest", json={"source": _SOURCE, "facts": facts})
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_ingest_valid_and_empty_payload(client):
|
|
facts = [{"fact_type": "history_context", "confidence": 0.6, "payload": {
|
|
"subject": "Wisteria", "origin": "China", "period": "Tang Dynasty", "summary": "Introduced to Europe in 1816.",
|
|
}}]
|
|
resp = client.post("/api/v1/knowledge/ingest", json={"source": _SOURCE, "facts": facts})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["facts_inserted"] == 1
|
|
|
|
|
|
def test_ingest_source_is_upserted_across_calls(client):
|
|
facts = [{"fact_type": "instruction", "confidence": 0.8, "payload": {
|
|
"action": "dig", "subject": "bed", "steps": [], "timing": "autumn",
|
|
}}]
|
|
r1 = client.post("/api/v1/knowledge/ingest", json={"source": _SOURCE, "facts": facts}).json()
|
|
r2 = client.post("/api/v1/knowledge/ingest", json={"source": _SOURCE, "facts": facts}).json()
|
|
assert r1["source_id"] == r2["source_id"]
|
|
|
|
|
|
def test_ingest_subject_extracted_for_companion_planting(client):
|
|
facts = [{"fact_type": "companion_planting", "confidence": 0.95, "payload": {
|
|
"plants": ["basil", "tomato"], "relationship": "beneficial",
|
|
"benefit": "repels aphids", "spacing": None, "notes": None,
|
|
}}]
|
|
resp = client.post("/api/v1/knowledge/ingest", json={"source": _SOURCE, "facts": facts})
|
|
assert resp.status_code == 200
|
|
|
|
query = client.get("/api/v1/knowledge?fact_type=companion_planting")
|
|
assert query.status_code == 200
|
|
facts_out = query.json()
|
|
assert len(facts_out) == 1
|
|
assert facts_out[0]["subject"] == "basil"
|