ghostexec / tests /test_phase3.py
modelbuilderhq's picture
Upload folder using huggingface_hub
ff293b1 verified
"""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)
@pytest.mark.parametrize(
"action,check",
[
(
GhostexecAction(action_type="reply_email", email_id="e05", message_body="On it."),
lambda env: next(e for e in env.world.emails if e.id == "e05").replied is True,
),
(
GhostexecAction(action_type="archive_email", email_id="e09"),
lambda env: next(e for e in env.world.emails if e.id == "e09").read is True,
),
(
GhostexecAction(
action_type="reschedule_meeting",
meeting_id="m03",
new_time="2026-04-21T18:00:00",
),
lambda env: next(m for m in env.world.meetings if m.id == "m03").start
== "2026-04-21T18:00:00",
),
(
GhostexecAction(
action_type="cancel_meeting",
meeting_id="m10",
reason="Merged into ops review",
),
lambda env: next(m for m in env.world.meetings if m.id == "m10").cancelled is True,
),
(
GhostexecAction(action_type="complete_task", task_id="t07"),
lambda env: next(t for t in env.world.tasks if t.id == "t07").status == "done",
),
(
GhostexecAction(
action_type="delegate_task",
task_id="t08",
contact_name="Jordan Lee",
),
lambda env: next(t for t in env.world.tasks if t.id == "t08").delegated_to == "Jordan Lee",
),
(
GhostexecAction(
action_type="send_message",
contact_name="Jamie Liu",
message_body="Thanks for the demo feedback.",
),
lambda env: any("message to Jamie Liu" in line for line in env.world.action_log),
),
(
GhostexecAction(action_type="do_nothing"),
lambda env: True,
),
],
)
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