Spaces:
Running
Running
| """Phase 3: plain-text briefing, eight legal actions, validation without crashes.""" | |
| from pathlib import Path | |
| import pytest | |
| from ghostexec.models import GhostexecAction | |
| from ghostexec.server.ghostexec_environment import GhostexecEnvironment | |
| ROOT = Path(__file__).resolve().parents[1] | |
| SCENARIO = ROOT / "scenarios" / "phase2_core.json" | |
| def _env() -> GhostexecEnvironment: | |
| e = GhostexecEnvironment(SCENARIO) | |
| e.reset() | |
| return e | |
| def test_briefing_is_plain_text_after_reset(): | |
| env = _env() | |
| obs = env.reset() | |
| text = obs.echoed_message | |
| assert "=== GHOSTEXEC BRIEFING" in text | |
| assert "UNREAD EMAILS" in text | |
| assert "CALENDAR CONFLICTS IN NEXT 4 HOURS" in text | |
| assert "CONTACTS TO WATCH" in text | |
| assert "OVERDUE OR DUE-SOON TASKS" in text | |
| assert "EXEC STRESS LEVEL" in text | |
| assert "STEPS REMAINING" in text | |
| assert obs.message_length == len(text) | |
| def test_each_legal_action_runs_without_crash(action, check): | |
| env = _env() | |
| obs = env.step(action) | |
| assert obs.echoed_message | |
| assert check(env) | |
| def test_reply_marks_email_handled(): | |
| env = _env() | |
| e = next(x for x in env.world.emails if x.id == "e14") | |
| assert not e.read | |
| env.step(GhostexecAction(action_type="reply_email", email_id="e14", message_body="Noted.")) | |
| e2 = next(x for x in env.world.emails if x.id == "e14") | |
| assert e2.read and e2.replied | |
| def test_invalid_actions_return_error_metadata_not_exception(): | |
| base = _env() | |
| r_do_nothing = base.step(GhostexecAction(action_type="do_nothing")).reward | |
| env = _env() | |
| obs = env.step(GhostexecAction(action_type="reply_email", email_id="nope", message_body="x")) | |
| assert obs.metadata.get("step_ok") is False | |
| assert obs.metadata.get("step_error") | |
| # Same before→after sub-scores as do_nothing, plus explicit invalid add-on. | |
| # do_nothing has an additional strict additive floor (-0.15), so the delta is -0.10 here. | |
| assert obs.reward == pytest.approx((r_do_nothing or 0) - (0.25 - 0.15)) | |
| obs2 = env.step(GhostexecAction(action_type="complete_task", task_id="t09")) | |
| assert obs2.metadata.get("step_ok") is False | |
| assert "already done" in (obs2.metadata.get("step_error") or "").lower() | |
| obs3 = env.step( | |
| GhostexecAction( | |
| action_type="send_message", | |
| contact_name="Nobody By That Name", | |
| message_body="hello", | |
| ) | |
| ) | |
| assert obs3.metadata.get("step_ok") is False | |
| obs4 = env.step( | |
| GhostexecAction( | |
| action_type="reschedule_meeting", | |
| meeting_id="m03", | |
| new_time="2026-04-21T09:30:00", | |
| ) | |
| ) | |
| assert obs4.metadata.get("step_ok") is False | |
| assert "overlap" in (obs4.metadata.get("step_error") or "").lower() | |
| def test_reschedule_resolves_prior_conflict_pair(): | |
| env = _env() | |
| before = {frozenset((r["meeting_a"], r["meeting_b"])) for r in env.detect_meeting_conflicts()} | |
| assert frozenset(("m01", "m02")) in before | |
| obs = env.step( | |
| GhostexecAction( | |
| action_type="reschedule_meeting", | |
| meeting_id="m02", | |
| new_time="2026-04-21T18:00:00", | |
| ) | |
| ) | |
| assert obs.metadata.get("step_ok") is True | |
| after = {frozenset((r["meeting_a"], r["meeting_b"])) for r in env.detect_meeting_conflicts()} | |
| assert frozenset(("m01", "m02")) not in after | |