Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 142 additions & 1 deletion tests/test_setup_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ def _download(src: str, dst: str) -> str:
return _download


def _isolate_home(tmp_path: Path, monkeypatch) -> Path:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
return home


def test_dedupe_config_destinations_preserves_first_destination() -> None:
configs = [
("repo-a", "configs/a.toml", "configs/out.toml"),
Expand All @@ -32,6 +39,7 @@ def test_run_setup_downloads_endpoints_toml_and_default_config_sets(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

downloaded: list[tuple[str, str]] = []
config_batches: list[list[tuple[str, str, str]]] = []
Expand All @@ -57,6 +65,7 @@ def test_run_setup_with_prime_rl_downloads_prime_configs_plus_shared_configs(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

downloaded: list[tuple[str, str]] = []
config_batches: list[list[tuple[str, str, str]]] = []
Expand Down Expand Up @@ -109,6 +118,7 @@ def test_prepare_agent_skill_dirs_materializes_skills_from_prime(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)
monkeypatch.setattr(setup, "LAB_SKILLS", ["create-environments", "brainstorm"])

for skill_name in setup.LAB_SKILLS:
Expand All @@ -118,6 +128,9 @@ def test_prepare_agent_skill_dirs_materializes_skills_from_prime(

setup._prepare_agent_skill_dirs(["codex"])

skills_dir = tmp_path / ".codex" / "skills"
if skills_dir.is_symlink():
assert skills_dir.resolve() == (tmp_path / ".prime" / "skills").resolve()
for skill_name in setup.LAB_SKILLS:
source = tmp_path / ".prime" / "skills" / skill_name
target = tmp_path / ".codex" / "skills" / skill_name
Expand All @@ -131,6 +144,7 @@ def test_prepare_agent_skill_dirs_is_safe_with_existing_links(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)
monkeypatch.setattr(setup, "LAB_SKILLS", ["create-environments"])

source = tmp_path / ".prime" / "skills" / "create-environments"
Expand All @@ -151,10 +165,94 @@ def test_prepare_agent_skill_dirs_is_safe_with_existing_links(
assert (target / "SKILL.md").exists()


def test_prepare_agent_skill_dirs_skips_user_scope_skills(
tmp_path: Path, monkeypatch, capsys
) -> None:
monkeypatch.chdir(tmp_path)
home = _isolate_home(tmp_path, monkeypatch)
monkeypatch.setattr(setup, "LAB_SKILLS", ["brainstorm"])

source = tmp_path / ".prime" / "skills" / "brainstorm"
source.mkdir(parents=True, exist_ok=True)
(source / "SKILL.md").write_text("bundled brainstorm\n")

user_skill = home / ".codex" / "skills" / "brainstorm"
user_skill.mkdir(parents=True)
(user_skill / "SKILL.md").write_text("user brainstorm\n")

setup._prepare_agent_skill_dirs(["codex"])

target = tmp_path / ".codex" / "skills" / "brainstorm"
assert not target.exists()
assert not target.is_symlink()
assert (user_skill / "SKILL.md").read_text() == "user brainstorm\n"

output = capsys.readouterr().out
assert "Skipped .codex/skills/brainstorm because user skill exists" in output


def test_prepare_agent_skill_dirs_removes_managed_link_when_user_skill_exists(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
home = _isolate_home(tmp_path, monkeypatch)
monkeypatch.setattr(setup, "LAB_SKILLS", ["brainstorm"])

source = tmp_path / ".prime" / "skills" / "brainstorm"
source.mkdir(parents=True, exist_ok=True)
(source / "SKILL.md").write_text("bundled brainstorm\n")

skills_dir = tmp_path / ".codex" / "skills"
skills_dir.parent.mkdir(parents=True, exist_ok=True)
skills_dir.symlink_to(
os.path.relpath(source.parent, start=skills_dir.parent),
target_is_directory=True,
)

user_skill = home / ".codex" / "skills" / "brainstorm"
user_skill.mkdir(parents=True)
(user_skill / "SKILL.md").write_text("user brainstorm\n")

setup._prepare_agent_skill_dirs(["codex"])

target = tmp_path / ".codex" / "skills" / "brainstorm"
assert skills_dir.exists()
assert not skills_dir.is_symlink()
assert not target.exists()
assert not target.is_symlink()
assert (user_skill / "SKILL.md").exists()


def test_prepare_agent_skill_dirs_keeps_existing_project_skill_with_user_skill(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
home = _isolate_home(tmp_path, monkeypatch)
monkeypatch.setattr(setup, "LAB_SKILLS", ["brainstorm"])

source = tmp_path / ".prime" / "skills" / "brainstorm"
source.mkdir(parents=True, exist_ok=True)
(source / "SKILL.md").write_text("bundled brainstorm\n")

target = tmp_path / ".codex" / "skills" / "brainstorm"
target.mkdir(parents=True)
(target / "SKILL.md").write_text("project brainstorm\n")

user_skill = home / ".codex" / "skills" / "brainstorm"
user_skill.mkdir(parents=True)
(user_skill / "SKILL.md").write_text("user brainstorm\n")

setup._prepare_agent_skill_dirs(["codex"])

assert (target / "SKILL.md").read_text() == "project brainstorm\n"
assert (user_skill / "SKILL.md").read_text() == "user brainstorm\n"


def test_prepare_agent_skill_dirs_uses_mapped_root_for_amp(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)
monkeypatch.setattr(setup, "LAB_SKILLS", ["create-environments"])

source = tmp_path / ".prime" / "skills" / "create-environments"
Expand All @@ -163,6 +261,9 @@ def test_prepare_agent_skill_dirs_uses_mapped_root_for_amp(

setup._prepare_agent_skill_dirs(["amp"])

skills_dir = tmp_path / ".agents" / "skills"
if skills_dir.is_symlink():
assert skills_dir.resolve() == (tmp_path / ".prime" / "skills").resolve()
assert (
tmp_path / ".agents" / "skills" / "create-environments" / "SKILL.md"
).exists()
Expand All @@ -173,6 +274,7 @@ def test_prepare_agent_skill_dirs_supports_skill_name_mapping(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)
monkeypatch.setattr(setup, "LAB_SKILLS", ["create-environments"])
monkeypatch.setattr(
setup,
Expand All @@ -194,8 +296,10 @@ def test_run_setup_prints_post_setup_call_to_action(
tmp_path: Path, monkeypatch, capsys
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

monkeypatch.setattr(setup.wget, "download", _fake_download_factory([]))
downloaded: list[tuple[str, str]] = []
monkeypatch.setattr(setup.wget, "download", _fake_download_factory(downloaded))
monkeypatch.setattr(setup, "download_configs", lambda *_: None)
monkeypatch.setattr(setup, "sync_prime_skills", lambda: None)

Expand All @@ -218,10 +322,45 @@ def test_run_setup_prints_post_setup_call_to_action(
assert "prime gepa run my-env -m openai/gpt-5-nano" in output


def test_run_setup_does_not_write_claude_md_for_default_codex_agent(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

downloaded: list[tuple[str, str]] = []
monkeypatch.setattr(setup.wget, "download", _fake_download_factory(downloaded))
monkeypatch.setattr(setup, "download_configs", lambda *_: None)
monkeypatch.setattr(setup, "sync_prime_skills", lambda: None)

setup.run_setup(skip_install=True, no_interactive=True)

assert (setup.CLAUDE_MD_SRC, setup.CLAUDE_MD_DST) not in downloaded
assert not (tmp_path / "CLAUDE.md").exists()


def test_run_setup_writes_claude_md_when_claude_agent_selected(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

downloaded: list[tuple[str, str]] = []
monkeypatch.setattr(setup.wget, "download", _fake_download_factory(downloaded))
monkeypatch.setattr(setup, "download_configs", lambda *_: None)
monkeypatch.setattr(setup, "sync_prime_skills", lambda: None)

setup.run_setup(skip_install=True, agents="claude", no_interactive=True)

assert (setup.CLAUDE_MD_SRC, setup.CLAUDE_MD_DST) in downloaded
assert (tmp_path / "CLAUDE.md").exists()


def test_run_setup_prints_prime_rl_post_setup_call_to_action(
tmp_path: Path, monkeypatch, capsys
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

monkeypatch.setattr(setup.wget, "download", _fake_download_factory([]))
monkeypatch.setattr(setup, "download_configs", lambda *_: None)
Expand All @@ -243,6 +382,7 @@ def test_run_setup_prints_prime_rl_post_setup_call_to_action(

def test_run_setup_persists_lab_choices_metadata(tmp_path: Path, monkeypatch) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

monkeypatch.setattr(setup.wget, "download", _fake_download_factory([]))
monkeypatch.setattr(setup, "download_configs", lambda *_: None)
Expand Down Expand Up @@ -270,6 +410,7 @@ def test_run_setup_persists_default_lab_choices_metadata(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.chdir(tmp_path)
_isolate_home(tmp_path, monkeypatch)

monkeypatch.setattr(setup.wget, "download", _fake_download_factory([]))
monkeypatch.setattr(setup, "download_configs", lambda *_: None)
Expand Down
86 changes: 82 additions & 4 deletions verifiers/scripts/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
AGENT_SKILLS_DIR_MAP: dict[str, str] = {
"amp": ".agents/skills",
}
AGENT_USER_SKILLS_DIR_MAP: dict[str, str] = {
"amp": "~/.agents/skills",
}
AGENT_SKILL_NAME_MAP: dict[str, dict[str, str]] = {}
SUPPORTED_AGENTS = ("codex", "claude", "cursor", "opencode", "amp")
PRIME_SKILLS_DIR = ".prime/skills"
Expand Down Expand Up @@ -323,10 +326,13 @@ def run_setup(
wget.download(AGENTS_MD_SRC, AGENTS_MD_DST)
print(f"\nDownloaded {AGENTS_MD_DST} from https://github.com/{VERIFIERS_REPO}")

if os.path.exists(CLAUDE_MD_DST):
os.remove(CLAUDE_MD_DST)
wget.download(CLAUDE_MD_SRC, CLAUDE_MD_DST)
print(f"\nDownloaded {CLAUDE_MD_DST} from https://github.com/{VERIFIERS_REPO}")
if "claude" in selected_agents:
if os.path.exists(CLAUDE_MD_DST):
os.remove(CLAUDE_MD_DST)
wget.download(CLAUDE_MD_SRC, CLAUDE_MD_DST)
print(
f"\nDownloaded {CLAUDE_MD_DST} from https://github.com/{VERIFIERS_REPO}"
)

if os.path.exists(ENVS_AGENTS_MD_DST):
os.remove(ENVS_AGENTS_MD_DST)
Expand Down Expand Up @@ -517,17 +523,75 @@ def _prepare_agent_skill_dirs(agents: list[str]) -> None:
prime_skills_dir = Path(PRIME_SKILLS_DIR)
for agent in agents:
skills_dir = _resolve_agent_skills_dir(agent)
user_conflicts = _collect_user_skill_conflicts(agent, prime_skills_dir)
if user_conflicts:
_remove_managed_skill_link(prime_skills_dir, skills_dir)

if _should_link_agent_skills_root(agent, skills_dir, user_conflicts):
skills_dir.parent.mkdir(parents=True, exist_ok=True)
_safe_link_or_copy_skill_dir(prime_skills_dir, skills_dir)
print(f"Prepared {skills_dir}")
continue

skills_dir.mkdir(parents=True, exist_ok=True)
for skill_name in LAB_SKILLS:
source_skill_dir = prime_skills_dir / skill_name
if not source_skill_dir.exists():
continue
target_skill_name = _resolve_agent_skill_name(agent, skill_name)
target_skill_dir = skills_dir / target_skill_name
user_skill_dir = _resolve_user_agent_skills_dir(agent) / target_skill_name
if _should_skip_project_skill_for_user_skill(
source_skill_dir, target_skill_dir, user_skill_dir
):
print(
f"Skipped {target_skill_dir} because user skill exists at {user_skill_dir}"
)
continue
_safe_link_or_copy_skill_dir(source_skill_dir, target_skill_dir)
print(f"Prepared {skills_dir}")


def _collect_user_skill_conflicts(agent: str, prime_skills_dir: Path) -> set[str]:
user_skills_dir = _resolve_user_agent_skills_dir(agent)
conflicts: set[str] = set()
for skill_name in LAB_SKILLS:
source_skill_dir = prime_skills_dir / skill_name
if not source_skill_dir.exists():
continue
target_skill_name = _resolve_agent_skill_name(agent, skill_name)
user_skill_dir = user_skills_dir / target_skill_name
if user_skill_dir.exists() or user_skill_dir.is_symlink():
conflicts.add(target_skill_name)
return conflicts


def _should_link_agent_skills_root(
agent: str, skills_dir: Path, user_conflicts: set[str]
) -> bool:
if user_conflicts:
return False
if AGENT_SKILL_NAME_MAP.get(agent):
return False
return not (skills_dir.exists() or skills_dir.is_symlink())


def _should_skip_project_skill_for_user_skill(
source: Path, target: Path, user_skill_dir: Path
) -> bool:
if not (user_skill_dir.exists() or user_skill_dir.is_symlink()):
return False
if _same_path(target, user_skill_dir):
return False
_remove_managed_skill_link(source, target)
return True


def _remove_managed_skill_link(source: Path, target: Path) -> None:
if target.is_symlink() and _same_path(target, source):
target.unlink()


def _safe_link_or_copy_skill_dir(source: Path, target: Path) -> None:
if target.exists() or target.is_symlink():
return
Expand All @@ -547,10 +611,24 @@ def _resolve_agent_skills_dir(agent: str) -> Path:
return Path(f".{agent}") / "skills"


def _resolve_user_agent_skills_dir(agent: str) -> Path:
mapped_dir = AGENT_USER_SKILLS_DIR_MAP.get(agent)
if mapped_dir is not None:
return Path(mapped_dir).expanduser()
return Path.home() / f".{agent}" / "skills"


def _resolve_agent_skill_name(agent: str, skill_name: str) -> str:
return AGENT_SKILL_NAME_MAP.get(agent, {}).get(skill_name, skill_name)


def _same_path(left: Path, right: Path) -> bool:
try:
return left.resolve(strict=False) == right.resolve(strict=False)
except OSError:
return False


def _sync_lab_metadata(
*, primary_agent: str, selected_agents: list[str], use_multiple_agents: bool
) -> None:
Expand Down
Loading