Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import pytest | |
| from unstuck.prompts import breakdown_prompt, repair_prompt | |
| from unstuck.schema import CATEGORIES, StepValidationError, _normalize_text, validate_steps_payload | |
| def test_valid_payload_parses_and_coerces_est_minutes_to_int() -> None: | |
| result = validate_steps_payload( | |
| { | |
| "steps": [ | |
| { | |
| "text": " Open the document ", | |
| "category": "admin", | |
| "est_minutes": 4.6, | |
| } | |
| ] | |
| } | |
| ) | |
| assert result.task == "" | |
| assert len(result.steps) == 1 | |
| assert result.steps[0].text == "Open the document" | |
| assert result.steps[0].category == "admin" | |
| assert result.steps[0].est_minutes == 5 | |
| assert isinstance(result.steps[0].est_minutes, int) | |
| def test_step_text_normalizes_whitespace_and_drops_one_trailing_period() -> None: | |
| result = validate_steps_payload( | |
| {"steps": [{"text": " Open the file. ", "category": "admin", "est_minutes": 3}]} | |
| ) | |
| assert result.steps[0].text == "Open the file" | |
| def test_step_text_keeps_ellipsis_when_normalizing() -> None: | |
| assert _normalize_text(" Wait... ") == "Wait..." | |
| def test_more_than_twelve_steps_raises_exact_repair_message() -> None: | |
| payload = { | |
| "steps": [ | |
| {"text": f"Open file {index}", "category": "admin", "est_minutes": 1} | |
| for index in range(13) | |
| ] | |
| } | |
| with pytest.raises(StepValidationError) as exc_info: | |
| validate_steps_payload(payload) | |
| assert str(exc_info.value) == "too many steps - return at most 12" | |
| def test_one_word_step_text_raises_exact_repair_message() -> None: | |
| with pytest.raises(StepValidationError) as exc_info: | |
| validate_steps_payload( | |
| {"steps": [{"text": "Email", "category": "admin", "est_minutes": 5}]} | |
| ) | |
| assert str(exc_info.value) == "step text must be a concrete action of at least two words" | |
| def test_step_text_over_ninety_characters_raises_exact_repair_message() -> None: | |
| with pytest.raises(StepValidationError) as exc_info: | |
| validate_steps_payload( | |
| { | |
| "steps": [ | |
| { | |
| "text": "Write the first full careful draft of the project update " | |
| "with every detailed note included before sending it to reviewers", | |
| "category": "deep-work", | |
| "est_minutes": 15, | |
| } | |
| ] | |
| } | |
| ) | |
| assert str(exc_info.value) == "step text too long - keep each step under 90 characters" | |
| def test_vague_step_starter_raises_exact_repair_message() -> None: | |
| with pytest.raises(StepValidationError) as exc_info: | |
| validate_steps_payload( | |
| {"steps": [{"text": "Work on report", "category": "deep-work", "est_minutes": 10}]} | |
| ) | |
| assert ( | |
| str(exc_info.value) | |
| == 'step "Work on report" is vague - start with a concrete physical action' | |
| ) | |
| def test_duplicate_step_text_raises_exact_repair_message() -> None: | |
| with pytest.raises(StepValidationError) as exc_info: | |
| validate_steps_payload( | |
| { | |
| "steps": [ | |
| {"text": "Open the file", "category": "admin", "est_minutes": 2}, | |
| {"text": "open the file.", "category": "admin", "est_minutes": 2}, | |
| ] | |
| } | |
| ) | |
| assert str(exc_info.value) == "steps must not repeat" | |
| def test_missing_steps_key_raises() -> None: | |
| with pytest.raises(StepValidationError): | |
| validate_steps_payload({}) | |
| def test_empty_steps_list_raises() -> None: | |
| with pytest.raises(StepValidationError): | |
| validate_steps_payload({"steps": []}) | |
| def test_bad_category_raises() -> None: | |
| with pytest.raises(StepValidationError): | |
| validate_steps_payload( | |
| {"steps": [{"text": "Email Sam", "category": "social", "est_minutes": 5}]} | |
| ) | |
| def test_non_positive_estimate_raises() -> None: | |
| with pytest.raises(StepValidationError): | |
| validate_steps_payload( | |
| {"steps": [{"text": "Email Sam", "category": "admin", "est_minutes": 0}]} | |
| ) | |
| def test_estimate_over_25_minutes_raises() -> None: | |
| with pytest.raises(StepValidationError): | |
| validate_steps_payload( | |
| {"steps": [{"text": "Draft report", "category": "deep-work", "est_minutes": 40}]} | |
| ) | |
| def test_blank_text_raises() -> None: | |
| with pytest.raises(StepValidationError): | |
| validate_steps_payload( | |
| {"steps": [{"text": " ", "category": "admin", "est_minutes": 5}]} | |
| ) | |
| def test_categories_match_contract() -> None: | |
| assert set(CATEGORIES) == {"admin", "creative", "errand", "deep-work"} | |
| def test_breakdown_prompt_includes_task_json_categories_and_time_limit() -> None: | |
| prompt = breakdown_prompt("write the quarterly review") | |
| assert "write the quarterly review" in prompt | |
| assert "json" in prompt.lower() | |
| for category in CATEGORIES: | |
| assert category in prompt | |
| assert "25" in prompt | |
| def test_repair_prompt_includes_bad_output_and_error() -> None: | |
| prompt = repair_prompt("task x", "GARBLED{", "no JSON object found") | |
| assert "GARBLED{" in prompt | |
| assert "no JSON object found" in prompt | |
| def test_new_vague_starters_rejected(text: str) -> None: | |
| with pytest.raises(StepValidationError, match="vague"): | |
| validate_steps_payload( | |
| {"steps": [{"text": text, "category": "admin", "est_minutes": 5}]} | |
| ) | |