diff --git a/agents/README.md b/agents/README.md index d75f596b..fdd34227 100644 --- a/agents/README.md +++ b/agents/README.md @@ -10,6 +10,7 @@ The primary workflow is to symlink files from here into each agent runtime/confi - `AGENTS.md`: global baseline rules for coding agents. - `evals/global-rules.md`: behavioral scenarios for validating changes to the global rules. - `install-rules.py`: installs the baseline by creating symlinks in supported agent config dirs. +- `test_install_rules.py`: regression tests for file preservation and independent target handling. - `install-cli.md`: curated CLI install/update command snippets. - `install-skills.md`: curated `npx skills` command snippets. @@ -38,6 +39,7 @@ Current targets: Behavior: - Each target is handled independently. +- Target errors are reported while remaining targets are attempted; any error yields a nonzero exit. - Missing destination directories are skipped. - Existing regular files are preserved as `.bak` backups (numbered when a backup already exists). - Destination links are replaced atomically; a failed link creation leaves the destination intact. @@ -49,6 +51,12 @@ the global rules source and is not installed by this script. Auto-approval controls tool prompting. The global rules still define task authorization, safety, and secret handling. +Run installer regression tests without changing installed rules: + +```bash +python3 -B -m unittest discover -s agents -p test_install_rules.py +``` + ## About `install-cli.md` and `install-skills.md` Use them as snippet libraries: diff --git a/agents/install-rules.py b/agents/install-rules.py index bf898224..a257f9da 100644 --- a/agents/install-rules.py +++ b/agents/install-rules.py @@ -47,12 +47,21 @@ def main() -> int: opencode_dir = xdg_config_home / "opencode" claude_dir = Path("~/.claude").expanduser() agents_dir = Path("~/.agents").expanduser() - install_one(codex_dir, agents_file, "AGENTS.md") - install_one(opencode_dir, agents_file, "AGENTS.md") - install_one(claude_dir, agents_file, "CLAUDE.md") - install_one(agents_dir, agents_file, "AGENTS.md") + targets = ( + (codex_dir, "AGENTS.md"), + (opencode_dir, "AGENTS.md"), + (claude_dir, "CLAUDE.md"), + (agents_dir, "AGENTS.md"), + ) + failed = False + for target_dir, target_name in targets: + try: + install_one(target_dir, agents_file, target_name) + except OSError as error: + print(f"failed {target_dir / target_name}: {error}", file=sys.stderr) + failed = True - return 0 + return 1 if failed else 0 if __name__ == "__main__": diff --git a/agents/test_install_rules.py b/agents/test_install_rules.py index 2f1c41cf..6f90aca5 100644 --- a/agents/test_install_rules.py +++ b/agents/test_install_rules.py @@ -1,4 +1,5 @@ import importlib.util +import io import tempfile import unittest from pathlib import Path @@ -13,6 +14,22 @@ spec.loader.exec_module(installer) class InstallRulesTests(unittest.TestCase): + def test_target_failure_does_not_stop_remaining_targets(self): + with ( + patch.object( + installer, "install_one", side_effect=[PermissionError("denied"), None, None, None] + ) as install, + patch("sys.stderr", new_callable=io.StringIO) as stderr, + ): + self.assertEqual(installer.main(), 1) + self.assertEqual(install.call_count, 4) + self.assertIn("denied", stderr.getvalue()) + + def test_successful_targets_return_success(self): + with patch.object(installer, "install_one") as install: + self.assertEqual(installer.main(), 0) + self.assertEqual(install.call_count, 4) + def test_existing_file_is_backed_up_without_overwriting_backup(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory)