Advertisement

← refacto-feature-scout

tests/test_projects.py

6.6 KB Download file
import json

import pytest

from scout.projects import (ConfigProblem, PROJECT_FILE, from_dict, load_projects, register,
                            read_project_file, resolve, validate)

GOOD = {
    "version": 1, "key": "mealplan", "name": "Meal Planner",
    "what_it_is": "Turns a shopping list into meals.",
    "who_uses_it": "Home cooks.",
    "sources": {"reddit": ["https://www.reddit.com/r/mealprep/new/.rss"], "urls": ["https://example.com/feedback"]},
    "test_command": "npm test", "link_dirs": ["node_modules"],
}


def write_project(folder, data=None, git=True):
    folder.mkdir(parents=True, exist_ok=True)
    (folder / PROJECT_FILE).write_text(json.dumps(data or GOOD))
    if git:
        (folder / ".git").mkdir(exist_ok=True)
    return folder


def test_a_projects_settings_are_read_from_its_own_folder(tmp_path):
    folder = write_project(tmp_path / "mealplan")
    p = read_project_file(folder)
    assert p.key == "mealplan" and p.name == "Meal Planner"
    assert p.test_command == "npm test" and p.link_dirs == ("node_modules",)
    assert p.reddit_feeds == ["https://www.reddit.com/r/mealprep/new/.rss"]
    assert p.signal_urls == ["https://example.com/feedback"]
    assert p.description == "Turns a shopping list into meals. Home cooks."


def test_settings_written_by_an_older_copy_of_the_tool_still_load(tmp_path):
    old = {"name": "Old App", "repo": "ignored", "description": "It does things.",
           "venue": "Polymarket and Kalshi", "test_cmd": "pytest", "deploy_cmd": "fly deploy",
           "node_module_dirs": ["node_modules"], "signal_urls": ["https://board"]}
    p = from_dict(old, tmp_path, key="old")
    assert p.what_it_is == "It does things." and p.test_command == "pytest"
    assert p.areas == ["Polymarket", "Kalshi"] and p.venue == "Polymarket and Kalshi"
    assert p.deploy_command == "fly deploy" and p.link_dirs == ("node_modules",)
    assert p.signal_urls == ["https://board"]


def test_a_key_in_the_settings_file_is_refused(tmp_path):
    """The file is saved with the project, so a key in it would end up shared."""
    with pytest.raises(ConfigProblem) as e:
        from_dict({**GOOD, "anthropic_api_key": "sk-ant-secret"}, tmp_path)
    assert "anthropic api key" in str(e.value).lower()
    assert "sk-ant-secret" not in str(e.value)


def test_settings_from_a_newer_version_do_not_break_an_older_one(tmp_path):
    p = from_dict({**GOOD, "something_invented_later": True}, tmp_path)
    assert p.name == "Meal Planner"


def test_broken_json_says_which_file(tmp_path):
    folder = tmp_path / "app"
    folder.mkdir()
    (folder / PROJECT_FILE).write_text("{not json")
    with pytest.raises(ConfigProblem) as e:
        read_project_file(folder)
    assert PROJECT_FILE in str(e.value)


def test_validate_reports_problems_in_plain_words(tmp_path):
    missing = from_dict(GOOD, tmp_path / "nowhere")
    assert any("can't find the folder" in m for m in validate(missing))

    folder = write_project(tmp_path / "untracked", git=False)
    assert any("isn't saved with git" in m for m in validate(from_dict(GOOD, folder)))

    folder = write_project(tmp_path / "app2", {**GOOD, "context_files": ["MISSING.md"]})
    assert any("MISSING.md" in m for m in validate(read_project_file(folder)))

    assert validate(read_project_file(write_project(tmp_path / "fine"))) == []


def test_a_bad_short_name_is_explained(tmp_path):
    folder = write_project(tmp_path / "app3", {**GOOD, "key": "Not A Key"})
    assert any("won't work as a short name" in m for m in validate(read_project_file(folder)))


# ---- the machine's list of projects ----

def test_registering_a_project_makes_it_findable(tmp_path):
    folder = write_project(tmp_path / "mealplan")
    registry = tmp_path / "projects.json"
    register("mealplan", folder, path=registry)
    assert list(load_projects(registry)) == ["mealplan"]
    assert resolve("mealplan", path=registry).name == "Meal Planner"


def test_settings_can_live_in_the_list_itself(tmp_path):
    """How the operator's own cloud machine is set up: the code there is a throwaway copy,
    so its settings cannot live beside it."""
    registry = tmp_path / "projects.json"
    registry.write_text(json.dumps({"version": 1, "default": "terminal", "projects": {
        "terminal": {"folder": "/data/repos/terminal", "settings": {"name": "Refacto Terminal"}}}}))
    p = load_projects(registry)["terminal"]
    assert p.name == "Refacto Terminal" and str(p.folder) == "/data/repos/terminal"


def test_the_project_you_are_standing_in_is_the_one_that_is_meant(tmp_path):
    folder = write_project(tmp_path / "mealplan")
    registry = tmp_path / "projects.json"
    register("mealplan", folder, path=registry)
    write_project(tmp_path / "other", {**GOOD, "key": "other", "name": "Other"})
    register("other", tmp_path / "other", path=registry)
    assert resolve(cwd=folder, path=registry).key == "mealplan"
    assert resolve(cwd=folder / "deeper" if (folder / "deeper").exists() else folder,
                   path=registry).key == "mealplan"


def test_more_than_one_project_and_no_hint_asks_which(tmp_path):
    registry = tmp_path / "projects.json"
    for key in ("one", "two"):
        write_project(tmp_path / key, {**GOOD, "key": key, "name": key})
        register(key, tmp_path / key, path=registry)
    reg = json.loads(registry.read_text()); reg["default"] = None; registry.write_text(json.dumps(reg))
    with pytest.raises(ConfigProblem) as e:
        resolve(cwd=tmp_path, path=registry)
    assert "--project" in str(e.value) and "one, two" in str(e.value)


def test_no_projects_at_all_says_how_to_start(tmp_path):
    with pytest.raises(ConfigProblem) as e:
        resolve(cwd=tmp_path, path=tmp_path / "nothing.json")
    assert "feature-scout init" in str(e.value)


def test_an_unknown_name_lists_the_ones_that_exist(tmp_path):
    registry = tmp_path / "projects.json"
    register("mealplan", write_project(tmp_path / "mealplan"), path=registry)
    with pytest.raises(ConfigProblem) as e:
        resolve("nope", path=registry)
    assert "mealplan" in str(e.value)


def test_never_run_is_read_from_the_settings_file(tmp_path):
    import json
    from scout.projects import PROJECT_FILE, read_project_file
    (tmp_path / PROJECT_FILE).write_text(json.dumps({"name": "K", "never_run": ["Bash(npx playwright*)"]}))
    assert read_project_file(tmp_path).never_run == ["Bash(npx playwright*)"]
    (tmp_path / PROJECT_FILE).write_text(json.dumps({"name": "K"}))
    assert read_project_file(tmp_path).never_run == []