diff --git a/app/wizard/step_hardware.py b/app/wizard/step_hardware.py new file mode 100644 index 0000000..339272a --- /dev/null +++ b/app/wizard/step_hardware.py @@ -0,0 +1,14 @@ +"""Step 1 — Hardware detection and inference profile selection.""" + +PROFILES = ["remote", "cpu", "single-gpu", "dual-gpu"] + + +def validate(data: dict) -> list[str]: + """Return list of validation errors. Empty list = step passes.""" + errors = [] + profile = data.get("inference_profile", "") + if not profile: + errors.append("Inference profile is required.") + elif profile not in PROFILES: + errors.append(f"Invalid inference profile '{profile}'. Choose: {', '.join(PROFILES)}.") + return errors diff --git a/app/wizard/step_identity.py b/app/wizard/step_identity.py new file mode 100644 index 0000000..644a902 --- /dev/null +++ b/app/wizard/step_identity.py @@ -0,0 +1,13 @@ +"""Step 3 — Identity (name, email, phone, linkedin, career_summary).""" + + +def validate(data: dict) -> list[str]: + """Return list of validation errors. Empty list = step passes.""" + errors = [] + if not (data.get("name") or "").strip(): + errors.append("Full name is required.") + if not (data.get("email") or "").strip(): + errors.append("Email address is required.") + if not (data.get("career_summary") or "").strip(): + errors.append("Career summary is required.") + return errors diff --git a/app/wizard/step_inference.py b/app/wizard/step_inference.py new file mode 100644 index 0000000..5df54c8 --- /dev/null +++ b/app/wizard/step_inference.py @@ -0,0 +1,9 @@ +"""Step 5 — LLM inference backend configuration and key entry.""" + + +def validate(data: dict) -> list[str]: + """Return list of validation errors. Empty list = step passes.""" + errors = [] + if not data.get("endpoint_confirmed"): + errors.append("At least one working LLM endpoint must be confirmed.") + return errors diff --git a/app/wizard/step_resume.py b/app/wizard/step_resume.py new file mode 100644 index 0000000..705452b --- /dev/null +++ b/app/wizard/step_resume.py @@ -0,0 +1,10 @@ +"""Step 4 — Resume (upload or guided form builder).""" + + +def validate(data: dict) -> list[str]: + """Return list of validation errors. Empty list = step passes.""" + errors = [] + experience = data.get("experience") or [] + if not experience: + errors.append("At least one work experience entry is required.") + return errors diff --git a/app/wizard/step_search.py b/app/wizard/step_search.py new file mode 100644 index 0000000..e64633c --- /dev/null +++ b/app/wizard/step_search.py @@ -0,0 +1,13 @@ +"""Step 6 — Job search preferences (titles, locations, boards, keywords).""" + + +def validate(data: dict) -> list[str]: + """Return list of validation errors. Empty list = step passes.""" + errors = [] + titles = data.get("job_titles") or [] + locations = data.get("locations") or [] + if not titles: + errors.append("At least one job title is required.") + if not locations: + errors.append("At least one location is required.") + return errors diff --git a/app/wizard/step_tier.py b/app/wizard/step_tier.py new file mode 100644 index 0000000..1ca74e6 --- /dev/null +++ b/app/wizard/step_tier.py @@ -0,0 +1,13 @@ +"""Step 2 — Tier selection (free / paid / premium).""" +from app.wizard.tiers import TIERS + + +def validate(data: dict) -> list[str]: + """Return list of validation errors. Empty list = step passes.""" + errors = [] + tier = data.get("tier", "") + if not tier: + errors.append("Tier selection is required.") + elif tier not in TIERS: + errors.append(f"Invalid tier '{tier}'. Choose: {', '.join(TIERS)}.") + return errors diff --git a/tests/test_wizard_steps.py b/tests/test_wizard_steps.py new file mode 100644 index 0000000..37b6a87 --- /dev/null +++ b/tests/test_wizard_steps.py @@ -0,0 +1,112 @@ +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +# ── Hardware ─────────────────────────────────────────────────────────────────── +from app.wizard.step_hardware import validate as hw_validate, PROFILES + +def test_hw_valid(): + assert hw_validate({"inference_profile": "remote"}) == [] + +def test_hw_missing(): + assert hw_validate({}) != [] + +def test_hw_invalid(): + assert hw_validate({"inference_profile": "turbo"}) != [] + +def test_hw_all_profiles(): + for p in PROFILES: + assert hw_validate({"inference_profile": p}) == [] + +# ── Tier ─────────────────────────────────────────────────────────────────────── +from app.wizard.step_tier import validate as tier_validate + +def test_tier_valid(): + assert tier_validate({"tier": "free"}) == [] + +def test_tier_missing(): + assert tier_validate({}) != [] + +def test_tier_invalid(): + assert tier_validate({"tier": "enterprise"}) != [] + +# ── Identity ─────────────────────────────────────────────────────────────────── +from app.wizard.step_identity import validate as id_validate + +def test_id_all_required_fields(): + d = {"name": "Alice", "email": "a@b.com", "career_summary": "10 years of stuff."} + assert id_validate(d) == [] + +def test_id_missing_name(): + d = {"name": "", "email": "a@b.com", "career_summary": "x"} + errors = id_validate(d) + assert errors != [] + assert any("name" in e.lower() for e in errors) + +def test_id_missing_email(): + d = {"name": "Alice", "email": "", "career_summary": "x"} + errors = id_validate(d) + assert errors != [] + assert any("email" in e.lower() for e in errors) + +def test_id_missing_summary(): + d = {"name": "Alice", "email": "a@b.com", "career_summary": ""} + errors = id_validate(d) + assert errors != [] + assert any("summary" in e.lower() or "career" in e.lower() for e in errors) + +def test_id_whitespace_only_name(): + d = {"name": " ", "email": "a@b.com", "career_summary": "x"} + assert id_validate(d) != [] + +# ── Resume ───────────────────────────────────────────────────────────────────── +from app.wizard.step_resume import validate as resume_validate + +def test_resume_no_experience(): + assert resume_validate({"experience": []}) != [] + +def test_resume_one_entry(): + d = {"experience": [{"company": "Acme", "title": "Engineer", "bullets": ["did stuff"]}]} + assert resume_validate(d) == [] + +def test_resume_missing_experience_key(): + assert resume_validate({}) != [] + +# ── Inference ────────────────────────────────────────────────────────────────── +from app.wizard.step_inference import validate as inf_validate + +def test_inference_not_confirmed(): + assert inf_validate({"endpoint_confirmed": False}) != [] + +def test_inference_confirmed(): + assert inf_validate({"endpoint_confirmed": True}) == [] + +def test_inference_missing(): + assert inf_validate({}) != [] + +# ── Search ───────────────────────────────────────────────────────────────────── +from app.wizard.step_search import validate as search_validate + +def test_search_valid(): + d = {"job_titles": ["Software Engineer"], "locations": ["Remote"]} + assert search_validate(d) == [] + +def test_search_missing_titles(): + d = {"job_titles": [], "locations": ["Remote"]} + errors = search_validate(d) + assert errors != [] + assert any("title" in e.lower() for e in errors) + +def test_search_missing_locations(): + d = {"job_titles": ["SWE"], "locations": []} + errors = search_validate(d) + assert errors != [] + assert any("location" in e.lower() for e in errors) + +def test_search_missing_both(): + errors = search_validate({}) + assert len(errors) == 2 + +def test_search_none_values(): + d = {"job_titles": None, "locations": None} + assert search_validate(d) != []