Spaces:
Sleeping
Sleeping
| """ChatPlan mapper and import/export round-trip tests.""" | |
| from __future__ import annotations | |
| from datetime import date | |
| from app.chat_plan import ( | |
| ChatPlanError, | |
| chat_plan_to_day_put, | |
| plan_and_feedback_to_chat_plan, | |
| ) | |
| from app.schedule_store import DayPlan, DayReview, ScheduledBlock | |
| SAMPLE = { | |
| "schema_version": 1, | |
| "date": "2026-07-24", | |
| "title": "Spain pack day", | |
| "intention": "AXA + stay proof + hold PDF. No embassy. No flight pay.", | |
| "constraints": ["wallet home on walk", "Weeknd only after proof"], | |
| "blocks": [ | |
| { | |
| "start": "08:00", | |
| "end": "08:25", | |
| "title": "Interrupt walk (no headphones)", | |
| "priority": "P0", | |
| "kind": "body_care", | |
| "intent": "measure", | |
| "notes": "Eyes outside.", | |
| "status": "planned", | |
| "review": { | |
| "did": None, | |
| "minutes": None, | |
| "comment": "", | |
| "emotions": [], | |
| "fse_event": "", | |
| "intensity": None, | |
| }, | |
| }, | |
| { | |
| "start": "09:00", | |
| "end": "10:30", | |
| "title": "Fix AXA residence Tanzania", | |
| "priority": "P0", | |
| "kind": "admin_spain", | |
| "intent": "duty", | |
| "notes": "26 Aug–5 Sep 2026; PDF", | |
| "locked": True, | |
| "status": "planned", | |
| "review": { | |
| "did": None, | |
| "minutes": None, | |
| "comment": "", | |
| "emotions": [], | |
| "fse_event": "", | |
| "intensity": None, | |
| }, | |
| }, | |
| ], | |
| "day_review": { | |
| "comment": "", | |
| "emotions": [], | |
| "fse_events": "", | |
| "what_moved": "", | |
| "what_avoided": "", | |
| "tomorrow_change": "", | |
| }, | |
| } | |
| def test_chat_to_put_basic() -> None: | |
| put, fbs, warnings = chat_plan_to_day_put(SAMPLE, "2026-07-24") | |
| assert put.title == "Spain pack day" | |
| assert put.intention.startswith("AXA") | |
| assert put.constraints == ["wallet home on walk", "Weeknd only after proof"] | |
| assert len(put.blocks) == 2 | |
| assert put.blocks[0].planned_min == 25 | |
| assert put.blocks[1].kind == "admin_spain" | |
| assert put.source == "chat" | |
| assert fbs == [] | |
| assert isinstance(warnings, list) | |
| def test_date_mismatch() -> None: | |
| try: | |
| chat_plan_to_day_put(SAMPLE, "2026-07-25") | |
| assert False, "expected error" | |
| except ChatPlanError as exc: | |
| assert any("date mismatch" in e for e in exc.errors) | |
| def test_empty_blocks() -> None: | |
| try: | |
| chat_plan_to_day_put({"blocks": []}, "2026-07-24") | |
| assert False | |
| except ChatPlanError: | |
| pass | |
| def test_unknown_kind_coerced() -> None: | |
| chat = { | |
| "blocks": [ | |
| { | |
| "start": "10:00", | |
| "end": "10:30", | |
| "title": "Weird", | |
| "kind": "embassy_run", | |
| "priority": "P1", | |
| } | |
| ] | |
| } | |
| put, _, warnings = chat_plan_to_day_put(chat, "2026-07-24") | |
| assert put.blocks[0].kind == "other" | |
| assert "[kind:embassy_run]" in put.blocks[0].notes | |
| assert any("embassy_run" in w for w in warnings) | |
| def test_overlap_validation() -> None: | |
| chat = { | |
| "blocks": [ | |
| {"start": "09:00", "end": "10:00", "title": "A", "kind": "other"}, | |
| {"start": "09:30", "end": "10:30", "title": "B", "kind": "other"}, | |
| ] | |
| } | |
| try: | |
| chat_plan_to_day_put(chat, "2026-07-24") | |
| assert False | |
| except ChatPlanError as exc: | |
| assert any("overlap" in e for e in exc.errors) | |
| def test_export_round_trip_fields() -> None: | |
| put, _, _ = chat_plan_to_day_put(SAMPLE, "2026-07-24") | |
| plan = DayPlan( | |
| date="2026-07-24", | |
| blocks=put.blocks, | |
| title=put.title or "", | |
| intention=put.intention or "", | |
| constraints=put.constraints or [], | |
| day_review=put.day_review or DayReview(), | |
| source="chat", | |
| ) | |
| # Annotate first block | |
| fb = { | |
| put.blocks[0].id: { | |
| "block_id": put.blocks[0].id, | |
| "did": "done", | |
| "actual_min": 20, | |
| "note": "walked", | |
| "emotions": ["calm"], | |
| "fse_event": "", | |
| "intensity": 3, | |
| } | |
| } | |
| plan.blocks[0] = plan.blocks[0].model_copy(update={"status": "done"}) | |
| out = plan_and_feedback_to_chat_plan(plan, fb) | |
| assert out["title"] == "Spain pack day" | |
| assert out["intention"].startswith("AXA") | |
| assert out["blocks"][0]["review"]["comment"] == "walked" | |
| assert out["blocks"][0]["review"]["emotions"] == ["calm"] | |
| assert out["summary"]["blocks_total"] == 2 | |
| assert out["summary"]["blocks_done"] == 1 | |
| assert out["summary"]["p0_total"] == 2 | |
| assert out["summary"]["p0_done"] == 1 | |
| assert out["summary"]["actual_min_sum"] == 20 | |
| def test_old_plan_without_day_review_loads() -> None: | |
| plan = DayPlan.model_validate( | |
| { | |
| "date": "2026-01-01", | |
| "blocks": [], | |
| "source": "user", | |
| } | |
| ) | |
| assert plan.day_review.comment == "" | |
| assert plan.title == "" | |
| assert plan.constraints == [] | |
| def test_review_did_sets_status() -> None: | |
| chat = { | |
| "blocks": [ | |
| { | |
| "start": "11:00", | |
| "end": "11:30", | |
| "title": "Done already", | |
| "kind": "other", | |
| "status": "planned", | |
| "review": {"did": "done", "minutes": 25, "comment": "ok", "emotions": ["hope"]}, | |
| } | |
| ] | |
| } | |
| put, fbs, _ = chat_plan_to_day_put(chat, "2026-07-24") | |
| assert put.blocks[0].status == "done" | |
| assert len(fbs) == 1 | |
| assert fbs[0].emotions == ["hope"] | |
| assert fbs[0].actual_min == 25 | |