Advertisement

← refacto-feature-scout

tests/test_init.py

7.4 KB Download file
"""Setting up a project, driven the way a person would drive it."""
import json
import subprocess

import pytest

from scout import init as initmod
from scout.claude_runner import ClaudeResult
from scout.init import run_init
from scout.llm import Access, NoLLMAccess
from scout.projects import PROJECT_FILE, load_projects


@pytest.fixture(autouse=True)
def their_own_claude_is_fine(monkeypatch):
    monkeypatch.setattr(initmod, "require_llm_access",
                        lambda *a, **k: Access("subscription", "your Claude login on this computer"))


def scripted(answers: list[str]):
    """Answers in order; anything past the end means 'just take the suggestion'."""
    left = list(answers)

    def ask(question: str, suggested: str = "") -> str:
        return (left.pop(0) if left else "") or suggested
    return ask


def a_project(tmp_path):
    folder = tmp_path / "mealplan"
    folder.mkdir()
    (folder / "package.json").write_text(json.dumps({"scripts": {"test": "vitest run"}}))
    (folder / "README.md").write_text("# Meal Planner")
    subprocess.run(["git", "init", "-q"], cwd=folder, check=True)
    return folder


def test_a_person_can_finish_setup_by_pressing_return(tmp_path, monkeypatch):
    folder = a_project(tmp_path)
    monkeypatch.setenv("SCOUT_PROJECTS_FILE", str(tmp_path / "projects.json"))
    said: list[str] = []

    project = run_init(
        ask=scripted([str(folder), "Meal Planner", "Turns a list into meals.",
                      "Home cooks.", "home cooks", "", "mealprep, cooking", "meal planner app"]),
        say=said.append, folder=folder,
        tester=lambda f, c: (True, "ok"),
        use_ai=False,
    )

    settings = json.loads((folder / PROJECT_FILE).read_text())
    assert settings["name"] == "Meal Planner"
    assert settings["test_command"] == "npm test"          # detected, not typed
    assert settings["install_hint"] == "npm ci"
    assert settings["sources"]["urls"] == []
    assert settings["sources"]["reddit"] == [
        "https://www.reddit.com/r/mealprep/new/.rss",
        "https://www.reddit.com/r/mealprep/top/.rss?t=week",
        "https://www.reddit.com/r/cooking/new/.rss",
        "https://www.reddit.com/r/cooking/top/.rss?t=week",
    ]
    assert settings["spend_limit_usd"] == 20.0
    assert settings["deploy_command"] is None              # nothing goes live unless asked
    assert project.key == "meal-planner"
    assert "meal-planner" in load_projects(tmp_path / "projects.json")
    assert any("feature-scout review" in line for line in said)


def test_community_names_can_be_typed_any_way_a_person_types_them(tmp_path, monkeypatch):
    folder = a_project(tmp_path)
    monkeypatch.setenv("SCOUT_PROJECTS_FILE", str(tmp_path / "projects.json"))
    run_init(ask=scripted([str(folder), "App", "does", "people", "people", "",
                           "r/one, /r/two, https://example.com/feed.rss"]),
             say=lambda m: None, folder=folder, tester=lambda f, c: (True, "ok"), use_ai=False)
    feeds = json.loads((folder / PROJECT_FILE).read_text())["sources"]["reddit"]
    assert "https://www.reddit.com/r/one/new/.rss" in feeds
    assert "https://www.reddit.com/r/two/new/.rss" in feeds
    assert "https://example.com/feed.rss" in feeds


def test_a_project_with_no_tests_is_told_what_that_means(tmp_path, monkeypatch):
    folder = tmp_path / "plain"
    folder.mkdir()
    subprocess.run(["git", "init", "-q"], cwd=folder, check=True)
    monkeypatch.setenv("SCOUT_PROJECTS_FILE", str(tmp_path / "projects.json"))
    said: list[str] = []
    run_init(ask=scripted([str(folder), "Plain", "does", "people", "people", "", "", ""]),
             say=said.append, folder=folder, use_ai=False)
    assert json.loads((folder / PROJECT_FILE).read_text())["test_command"] == ""
    assert any("can't check them" in line for line in said)


def test_tests_that_do_not_pass_right_now_are_reported_before_anything_relies_on_them(tmp_path, monkeypatch):
    folder = a_project(tmp_path)
    monkeypatch.setenv("SCOUT_PROJECTS_FILE", str(tmp_path / "projects.json"))
    said: list[str] = []
    run_init(ask=scripted([str(folder), "App", "does", "people", "people", "", "", "",
                           "npm test", "no"]),
             say=said.append, folder=folder,
             tester=lambda f, c: (False, "3 failing"), use_ai=False)
    assert any("didn't pass right now" in line for line in said)
    assert json.loads((folder / PROJECT_FILE).read_text())["test_command"] == ""


def test_putting_things_live_is_never_turned_on_without_being_asked(tmp_path, monkeypatch):
    folder = a_project(tmp_path)
    (folder / "fly.toml").write_text("app = 'x'")
    monkeypatch.setenv("SCOUT_PROJECTS_FILE", str(tmp_path / "projects.json"))
    run_init(ask=scripted([str(folder), "App", "does", "people", "people", "", "", ""]),
             say=lambda m: None, folder=folder, tester=lambda f, c: (True, "ok"), use_ai=False)
    assert json.loads((folder / PROJECT_FILE).read_text())["deploy_command"] is None

    run_init(ask=scripted([str(folder), "App", "does", "people", "people", "", "", "",
                           "npm test", "yes"]),
             say=lambda m: None, folder=folder, tester=lambda f, c: (True, "ok"), use_ai=False)
    assert json.loads((folder / PROJECT_FILE).read_text())["deploy_command"] == "fly deploy --remote-only"


def test_a_folder_not_saved_with_git_is_offered_a_starting_point(tmp_path, monkeypatch):
    folder = tmp_path / "loose"
    folder.mkdir()
    (folder / "app.py").write_text("print('hi')")
    monkeypatch.setenv("SCOUT_PROJECTS_FILE", str(tmp_path / "projects.json"))
    run_init(ask=scripted([str(folder), "yes", "Loose", "does", "people", "people", "", "", ""]),  # 2nd = set up git
             say=lambda m: None, folder=folder, use_ai=False)
    assert (folder / ".git").exists()


def test_reading_the_project_fills_the_answers_in(tmp_path, monkeypatch):
    folder = a_project(tmp_path)
    monkeypatch.setenv("SCOUT_PROJECTS_FILE", str(tmp_path / "projects.json"))
    drafted = {"name": "Meal Planner", "what_it_is": "It plans meals.", "who_uses_it": "Cooks.",
               "audience": "home cooks", "topic": ", a meal planning app",
               "communities": ["mealprep"], "search_terms": ["meal planning app"]}

    def runner(prompt, **kw):
        assert kw["allowed_tools"] == ["Read", "Glob", "Grep"]      # reading only
        return ClaudeResult(ok=True, text=json.dumps(drafted))

    run_init(ask=scripted([str(folder), "yes"]), say=lambda m: None, folder=folder,
             runner=runner, tester=lambda f, c: (True, "ok"))
    settings = json.loads((folder / PROJECT_FILE).read_text())
    assert settings["what_it_is"] == "It plans meals." and settings["audience"] == "home cooks"
    assert settings["topic"] == ", a meal planning app"
    assert settings["sources"]["reddit"][0] == "https://www.reddit.com/r/mealprep/new/.rss"


def test_nothing_is_asked_of_someone_who_has_no_claude_account(tmp_path, monkeypatch):
    folder = a_project(tmp_path)
    monkeypatch.setattr(initmod, "require_llm_access",
                        lambda *a, **k: (_ for _ in ()).throw(NoLLMAccess("no account")))
    asked = []
    with pytest.raises(NoLLMAccess):
        run_init(ask=lambda q, s="": asked.append(q) or s, say=lambda m: None, folder=folder,
                 use_ai=False)
    assert asked == []