Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions src/specify_cli/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2251,6 +2251,8 @@ def _remove_vibe_toml_entries(dst: Path) -> bool:
"""Remove Specify-marked Vibe TOML hook entries; delete the file if now empty.

Returns True if the file was deleted (no user content remained).

Leaves an unowned file untouched when no Specify-marked hook was removed.
"""
if not dst.exists():
return False
Expand All @@ -2272,6 +2274,8 @@ def _remove_vibe_toml_entries(dst: Path) -> bool:
existing,
flags=re.DOTALL,
)
if cleaned == existing:
return False
# If only whitespace/comments remain, the file had no user content
stripped = "\n".join(
line for line in cleaned.splitlines()
Expand Down
69 changes: 69 additions & 0 deletions tests/integrations/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,75 @@ def test_owned_only_config_still_deleted_on_teardown(self, tmp_path):
assert not config_path.exists()


class TestVibeTomlNoOpRemoval:
"""An unowned Vibe hooks.toml must survive no-op event cleanup intact."""

_FIXED_MTIME_NS = 1_700_000_000_123_456_789

def _user_hooks_file(self, tmp_path, content):
path = tmp_path / ".vibe" / "hooks.toml"
path.parent.mkdir(parents=True)
path.write_bytes(content)
os.utime(path, ns=(self._FIXED_MTIME_NS, self._FIXED_MTIME_NS))
return path, path.stat().st_mtime_ns

def _assert_untouched(self, path, original, original_mtime_ns):
assert path.exists()
assert path.read_bytes() == original
assert path.stat().st_mtime_ns == original_mtime_ns

def test_empty_events_leave_user_crlf_file_untracked_and_untouched(self, tmp_path):
from specify_cli.integrations import get_integration

integration = get_integration("vibe")
manifest = _claude_manifest(tmp_path)
original = b'user_option = "keep"\r\nsecond_option = true'
path, original_mtime_ns = self._user_hooks_file(tmp_path, original)

install_integration_events(integration, tmp_path, manifest, {})

self._assert_untouched(path, original, original_mtime_ns)
manifest.record_existing.assert_not_called()

def test_empty_events_preserve_comments_only_file(self, tmp_path):
from specify_cli.integrations import get_integration

original = b"# maintained by the user\n# no hooks yet\n"
path, original_mtime_ns = self._user_hooks_file(tmp_path, original)

install_integration_events(
get_integration("vibe"), tmp_path, _claude_manifest(tmp_path), {}
)

self._assert_untouched(path, original, original_mtime_ns)

def test_empty_events_preserve_whitespace_only_file(self, tmp_path):
from specify_cli.integrations import get_integration

original = b"\r\n \t\r\n"
path, original_mtime_ns = self._user_hooks_file(tmp_path, original)

install_integration_events(
get_integration("vibe"), tmp_path, _claude_manifest(tmp_path), {}
)

self._assert_untouched(path, original, original_mtime_ns)

def test_forced_teardown_preserves_unowned_file_with_manifest_claim(self, tmp_path):
from specify_cli.integrations import get_integration

integration = get_integration("vibe")
original = b'user_option = "keep"\r\n'
path, original_mtime_ns = self._user_hooks_file(tmp_path, original)
manifest = IntegrationManifest(integration.key, tmp_path, version="test")
manifest.record_existing(".vibe/hooks.toml")
manifest.save()

integration.teardown(tmp_path, manifest, force=True)

self._assert_untouched(path, original, original_mtime_ns)


# -- Opencode TS Plugin merging ---------------------------------------------

class TestOpencodePluginMerging:
Expand Down
Loading