← refacto-feature-scout
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 == []