peregrine/tests/test_wizard_flow.py
pyr0ball 1a74793804 feat: wizard orchestrator — 7 steps, LLM generation polling, crash recovery
Replaces the old 5-step wizard with a 7-step orchestrator that uses the
step modules built in Tasks 2-8. Steps 1-6 are mandatory (hardware, tier,
identity, resume, inference, search); step 7 (integrations) is optional.
Each Next click validates, writes wizard_step to user.yaml for crash recovery,
and resumes at the correct step on page reload. LLM generation buttons
submit wizard_generate tasks and poll via @st.fragment(run_every=3). Finish
sets wizard_complete=True, removes wizard_step, and calls apply_service_urls.

Adds tests/test_wizard_flow.py (7 tests) covering validate() chain, yaml
persistence helpers, and wizard state inference.
2026-02-25 09:10:51 -08:00

116 lines
4.5 KiB
Python

"""
Wizard flow logic tests — no Streamlit dependency.
Tests validate() chain, yaml persistence helpers, and wizard state inference.
"""
import sys
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).parent.parent))
# ── All mandatory steps validate correctly ────────────────────────────────────
def test_all_mandatory_steps_accept_minimal_valid_data():
"""Each step's validate() accepts the minimum required input."""
from app.wizard.step_hardware import validate as hw
from app.wizard.step_tier import validate as tier
from app.wizard.step_identity import validate as ident
from app.wizard.step_resume import validate as resume
from app.wizard.step_inference import validate as inf
from app.wizard.step_search import validate as search
assert hw({"inference_profile": "remote"}) == []
assert tier({"tier": "free"}) == []
assert ident({"name": "A", "email": "a@b.com", "career_summary": "x"}) == []
assert resume({"experience": [{"company": "X", "title": "T", "bullets": []}]}) == []
assert inf({"endpoint_confirmed": True}) == []
assert search({"job_titles": ["SWE"], "locations": ["Remote"]}) == []
def test_mandatory_steps_reject_empty_data():
"""Each step's validate() rejects completely empty input."""
from app.wizard.step_hardware import validate as hw
from app.wizard.step_tier import validate as tier
from app.wizard.step_identity import validate as ident
from app.wizard.step_resume import validate as resume
from app.wizard.step_inference import validate as inf
from app.wizard.step_search import validate as search
assert hw({}) != []
assert tier({}) != []
assert ident({}) != []
assert resume({}) != []
assert inf({}) != []
assert search({}) != []
# ── Yaml persistence helpers ──────────────────────────────────────────────────
def test_wizard_step_persists_to_yaml(tmp_path):
"""Writing wizard_step to user.yaml survives a reload."""
p = tmp_path / "user.yaml"
p.write_text(yaml.dump({
"name": "Test", "email": "t@t.com",
"career_summary": "x", "wizard_complete": False,
}))
# Simulate "write step 3 on Next"
data = yaml.safe_load(p.read_text()) or {}
data["wizard_step"] = 3
p.write_text(yaml.dump(data))
reloaded = yaml.safe_load(p.read_text())
assert reloaded["wizard_step"] == 3
assert reloaded["wizard_complete"] is False
def test_finish_sets_wizard_complete_and_removes_wizard_step(tmp_path):
"""After Finish, wizard_complete is True and wizard_step is absent."""
p = tmp_path / "user.yaml"
p.write_text(yaml.dump({
"name": "Test", "email": "t@t.com",
"career_summary": "x", "wizard_complete": False, "wizard_step": 6,
}))
# Simulate Finish action
data = yaml.safe_load(p.read_text()) or {}
data["wizard_complete"] = True
data.pop("wizard_step", None)
p.write_text(yaml.dump(data))
reloaded = yaml.safe_load(p.read_text())
assert reloaded["wizard_complete"] is True
assert "wizard_step" not in reloaded
def test_wizard_resume_step_inferred_from_yaml(tmp_path):
"""wizard_step in user.yaml determines which step to resume at."""
p = tmp_path / "user.yaml"
p.write_text(yaml.dump({
"name": "Test", "email": "t@t.com",
"career_summary": "x", "wizard_complete": False, "wizard_step": 4,
}))
data = yaml.safe_load(p.read_text()) or {}
# Wizard should resume at step 5 (last_completed + 1)
resume_at = data.get("wizard_step", 0) + 1
assert resume_at == 5
def test_wizard_complete_true_means_no_wizard(tmp_path):
"""If wizard_complete is True, the app should NOT show the wizard."""
p = tmp_path / "user.yaml"
p.write_text(yaml.dump({
"name": "Test", "email": "t@t.com",
"career_summary": "x", "wizard_complete": True,
}))
from scripts.user_profile import UserProfile
u = UserProfile(p)
assert u.wizard_complete is True
def test_wizard_incomplete_means_show_wizard(tmp_path):
"""If wizard_complete is False, the app SHOULD show the wizard."""
p = tmp_path / "user.yaml"
p.write_text(yaml.dump({
"name": "Test", "email": "t@t.com",
"career_summary": "x", "wizard_complete": False,
}))
from scripts.user_profile import UserProfile
u = UserProfile(p)
assert u.wizard_complete is False