|
1 | 1 | """Tests for OpencodeIntegration.""" |
2 | 2 |
|
3 | 3 | from specify_cli.integrations import get_integration |
| 4 | +from specify_cli.integrations.manifest import IntegrationManifest |
| 5 | +from specify_cli.integrations.opencode import _migrate_legacy_command_dir |
4 | 6 |
|
5 | 7 | from .test_integration_base_markdown import MarkdownIntegrationTests |
6 | 8 |
|
7 | 9 |
|
8 | 10 | class TestOpencodeIntegration(MarkdownIntegrationTests): |
9 | 11 | KEY = "opencode" |
10 | 12 | FOLDER = ".opencode/" |
11 | | - COMMANDS_SUBDIR = "command" |
12 | | - REGISTRAR_DIR = ".opencode/command" |
| 13 | + COMMANDS_SUBDIR = "commands" |
| 14 | + REGISTRAR_DIR = ".opencode/commands" |
13 | 15 | CONTEXT_FILE = "AGENTS.md" |
14 | 16 |
|
15 | 17 | def test_build_exec_args_uses_run_command_dispatch(self): |
@@ -57,3 +59,87 @@ def test_build_exec_args_keeps_plain_prompt_dispatch(self): |
57 | 59 | args = integration.build_exec_args("explain this repository", output_json=False) |
58 | 60 |
|
59 | 61 | assert args == ["opencode", "run", "explain this repository"] |
| 62 | + |
| 63 | + |
| 64 | +class TestOpencodeCommandMigration: |
| 65 | + """Test legacy .opencode/command → .opencode/commands migration.""" |
| 66 | + |
| 67 | + def test_removes_legacy_command_dir(self, tmp_path): |
| 68 | + """Legacy file is removed when byte-identical to counterpart.""" |
| 69 | + legacy = tmp_path / ".opencode" / "command" |
| 70 | + legacy.mkdir(parents=True) |
| 71 | + (legacy / "speckit.specify.md").write_text("identical content") |
| 72 | + new_dir = tmp_path / ".opencode" / "commands" |
| 73 | + new_dir.mkdir(parents=True) |
| 74 | + (new_dir / "speckit.specify.md").write_text("identical content") |
| 75 | + |
| 76 | + removed = _migrate_legacy_command_dir(tmp_path) |
| 77 | + |
| 78 | + assert removed == 1 |
| 79 | + assert not legacy.exists() |
| 80 | + |
| 81 | + def test_preserves_user_customized_legacy_command(self, tmp_path): |
| 82 | + """Legacy file with different content (user-customized) is preserved.""" |
| 83 | + legacy = tmp_path / ".opencode" / "command" |
| 84 | + legacy.mkdir(parents=True) |
| 85 | + (legacy / "speckit.specify.md").write_text("my customization") |
| 86 | + new_dir = tmp_path / ".opencode" / "commands" |
| 87 | + new_dir.mkdir(parents=True) |
| 88 | + (new_dir / "speckit.specify.md").write_text("canonical content") |
| 89 | + |
| 90 | + removed = _migrate_legacy_command_dir(tmp_path) |
| 91 | + |
| 92 | + assert removed == 0 # nothing was removed or moved |
| 93 | + assert (legacy / "speckit.specify.md").exists() # user file preserved |
| 94 | + assert legacy.exists() # dir not removed (still has content) |
| 95 | + |
| 96 | + def test_no_op_when_no_legacy_dir(self, tmp_path): |
| 97 | + removed = _migrate_legacy_command_dir(tmp_path) |
| 98 | + assert removed == 0 |
| 99 | + |
| 100 | + def test_moves_user_owned_files_to_new_dir(self, tmp_path): |
| 101 | + """Files without a counterpart in the new dir are moved, not deleted.""" |
| 102 | + legacy = tmp_path / ".opencode" / "command" |
| 103 | + legacy.mkdir(parents=True) |
| 104 | + (legacy / "my-custom-command.md").write_text("user content") |
| 105 | + new_dir = tmp_path / ".opencode" / "commands" |
| 106 | + new_dir.mkdir(parents=True) |
| 107 | + |
| 108 | + removed = _migrate_legacy_command_dir(tmp_path) |
| 109 | + |
| 110 | + assert removed == 1 |
| 111 | + assert not (legacy / "my-custom-command.md").exists() |
| 112 | + assert (new_dir / "my-custom-command.md").read_text() == "user content" |
| 113 | + |
| 114 | + def test_handles_symlink_without_following(self, tmp_path): |
| 115 | + """A symlink at the legacy path is unlinked, not traversed via rmtree.""" |
| 116 | + target = tmp_path / "real_dir" |
| 117 | + target.mkdir() |
| 118 | + (legacy_parent := tmp_path / ".opencode").mkdir(parents=True) |
| 119 | + legacy = legacy_parent / "command" |
| 120 | + legacy.symlink_to(target) |
| 121 | + |
| 122 | + removed = _migrate_legacy_command_dir(tmp_path) |
| 123 | + |
| 124 | + assert removed == 1 |
| 125 | + assert not legacy.exists() |
| 126 | + assert target.is_dir() # target is untouched |
| 127 | + |
| 128 | + def test_setup_removes_legacy_dir(self, tmp_path): |
| 129 | + """setup() preserves user-customized files and moves files without counterparts.""" |
| 130 | + legacy = tmp_path / ".opencode" / "command" |
| 131 | + legacy.mkdir(parents=True) |
| 132 | + (legacy / "speckit.specify.md").write_text("old content") |
| 133 | + (legacy / "my-custom.md").write_text("user content") |
| 134 | + |
| 135 | + i = get_integration("opencode") |
| 136 | + m = IntegrationManifest("opencode", tmp_path) |
| 137 | + i.setup(tmp_path, m) |
| 138 | + |
| 139 | + assert (tmp_path / ".opencode" / "commands").is_dir() |
| 140 | + # User-customized speckit.specify.md is preserved in legacy dir |
| 141 | + assert (legacy / "speckit.specify.md").exists() |
| 142 | + assert legacy.exists() # dir still exists because it has the customized file |
| 143 | + # my-custom.md has no counterpart, so it was moved to new dir |
| 144 | + assert not (legacy / "my-custom.md").exists() |
| 145 | + assert (tmp_path / ".opencode" / "commands" / "my-custom.md").read_text() == "user content" |
0 commit comments