fix: MCP plugin follows .beads/redirect files (bd-7t9a, gt-tnw)
The _find_beads_db_in_tree() function now follows .beads/redirect files to find shared beads databases. This is essential for polecat/crew directories that use redirect files to share a central database. Changes: - Added _resolve_beads_redirect() helper function - Updated _find_beads_db_in_tree() to check for redirect files before looking for local .db files - Added comprehensive tests for redirect functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -378,7 +378,7 @@
|
|||||||
{"id":"bd-haxi","title":"Restart running daemons","description":"Kill and restart any running bd daemons to pick up new version: pkill -f 'bd daemon' \u0026\u0026 bd daemon --start","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-21T13:52:33.066262-08:00","updated_at":"2025-12-21T13:53:49.757078-08:00","deleted_at":"2025-12-21T13:53:49.757078-08:00","deleted_by":"stevey","delete_reason":"manual delete","original_type":"task"}
|
{"id":"bd-haxi","title":"Restart running daemons","description":"Kill and restart any running bd daemons to pick up new version: pkill -f 'bd daemon' \u0026\u0026 bd daemon --start","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-21T13:52:33.066262-08:00","updated_at":"2025-12-21T13:53:49.757078-08:00","deleted_at":"2025-12-21T13:53:49.757078-08:00","deleted_by":"stevey","delete_reason":"manual delete","original_type":"task"}
|
||||||
{"id":"bd-haze","title":"Fix beads-9yc: pinned column missing from schema. gt mail...","description":"Fix beads-9yc: pinned column missing from schema. gt mail send fails because some beads DBs lack the pinned column. Add migration to ensure it exists.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T15:05:33.394801-08:00","updated_at":"2025-12-21T15:26:35.171757-08:00","closed_at":"2025-12-21T15:26:35.171757-08:00"}
|
{"id":"bd-haze","title":"Fix beads-9yc: pinned column missing from schema. gt mail...","description":"Fix beads-9yc: pinned column missing from schema. gt mail send fails because some beads DBs lack the pinned column. Add migration to ensure it exists.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T15:05:33.394801-08:00","updated_at":"2025-12-21T15:26:35.171757-08:00","closed_at":"2025-12-21T15:26:35.171757-08:00"}
|
||||||
{"id":"bd-hhv3","title":"Test and document molecular chemistry commands","description":"## Context\n\nImplemented the molecular chemistry UX commands per the design docs:\n- gastown/mayor/rig/docs/molecular-chemistry.md\n- gastown/mayor/rig/docs/chemistry-design-changes.md\n\nCommit: cadf798b\n\n## New Commands to Test\n\n| Command | Purpose |\n|---------|---------|\n| `bd pour \u003cproto\u003e` | Instantiate proto as persistent mol |\n| `bd wisp create \u003cproto\u003e` | Instantiate proto as ephemeral wisp |\n| `bd hook [--agent]` | Inspect what's on an agent's hook |\n\n## Enhanced Commands to Test\n\n| Command | Changes |\n|---------|---------|\n| `bd mol spawn --pour` | New flag, `--persistent` deprecated |\n| `bd mol bond --pour` | Force liquid phase on wisp target |\n| `bd pin --for \u003cagent\u003e --start` | Chemistry workflow support |\n\n## Test Scenarios\n\n1. **bd pour**: Create persistent mol from a proto\n - Verify creates in .beads/ (not .beads-wisp/)\n - Verify variable substitution works\n - Verify --dry-run works\n\n2. **bd wisp create**: Create ephemeral wisp from proto\n - Verify creates in .beads-wisp/\n - Verify bd wisp list shows it\n - Verify bd mol squash works\n - Verify bd mol burn works\n\n3. **bd hook**: Inspect pinned work\n - Pin something, verify bd hook shows it\n - Test --agent flag\n - Test --json output\n\n4. **bd pin --for**: Assign work to agent\n - Verify sets pinned=true\n - Verify sets assignee\n - Verify --start sets status=in_progress\n\n5. **bd mol bond --pour**: Force liquid on wisp target\n - Bond a proto to a wisp with --pour\n - Verify spawned issues are in .beads/\n\n## Documentation\n\n- Update CLAUDE.md with new commands\n- Add examples to --help output (already done)\n- Consider adding to docs/CLI_REFERENCE.md\n\n## Code Review\n\n- Check for edge cases\n- Verify error messages are helpful\n- Ensure --json output is consistent","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T02:22:10.906646-08:00","updated_at":"2025-12-22T02:55:37.983703-08:00","closed_at":"2025-12-22T02:55:37.983703-08:00"}
|
{"id":"bd-hhv3","title":"Test and document molecular chemistry commands","description":"## Context\n\nImplemented the molecular chemistry UX commands per the design docs:\n- gastown/mayor/rig/docs/molecular-chemistry.md\n- gastown/mayor/rig/docs/chemistry-design-changes.md\n\nCommit: cadf798b\n\n## New Commands to Test\n\n| Command | Purpose |\n|---------|---------|\n| `bd pour \u003cproto\u003e` | Instantiate proto as persistent mol |\n| `bd wisp create \u003cproto\u003e` | Instantiate proto as ephemeral wisp |\n| `bd hook [--agent]` | Inspect what's on an agent's hook |\n\n## Enhanced Commands to Test\n\n| Command | Changes |\n|---------|---------|\n| `bd mol spawn --pour` | New flag, `--persistent` deprecated |\n| `bd mol bond --pour` | Force liquid phase on wisp target |\n| `bd pin --for \u003cagent\u003e --start` | Chemistry workflow support |\n\n## Test Scenarios\n\n1. **bd pour**: Create persistent mol from a proto\n - Verify creates in .beads/ (not .beads-wisp/)\n - Verify variable substitution works\n - Verify --dry-run works\n\n2. **bd wisp create**: Create ephemeral wisp from proto\n - Verify creates in .beads-wisp/\n - Verify bd wisp list shows it\n - Verify bd mol squash works\n - Verify bd mol burn works\n\n3. **bd hook**: Inspect pinned work\n - Pin something, verify bd hook shows it\n - Test --agent flag\n - Test --json output\n\n4. **bd pin --for**: Assign work to agent\n - Verify sets pinned=true\n - Verify sets assignee\n - Verify --start sets status=in_progress\n\n5. **bd mol bond --pour**: Force liquid on wisp target\n - Bond a proto to a wisp with --pour\n - Verify spawned issues are in .beads/\n\n## Documentation\n\n- Update CLAUDE.md with new commands\n- Add examples to --help output (already done)\n- Consider adding to docs/CLI_REFERENCE.md\n\n## Code Review\n\n- Check for edge cases\n- Verify error messages are helpful\n- Ensure --json output is consistent","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T02:22:10.906646-08:00","updated_at":"2025-12-22T02:55:37.983703-08:00","closed_at":"2025-12-22T02:55:37.983703-08:00"}
|
||||||
{"id":"bd-hj0s","title":"Add 'convoy' issue type with reactive completion","description":"Add convoy as a new issue type with reactive completion semantics.\n\nBehavior:\n- Convoy has list of tracked issues (via 'tracks' relation)\n- When all tracked issues close (including wontfix), convoy auto-closes\n- Supports cross-prefix tracking (convoy in hq-* tracks gt-*, bd-*)\n\nImplementation:\n- New type: convoy\n- Reactive completion trigger on tracked issue closure\n- Query support: 'bd list --type=convoy'\n\nRelated: hq-7h8jx (Convoy System epic in town beads)","status":"hooked","priority":1,"issue_type":"task","assignee":"beads/crew/dave","created_at":"2025-12-29T18:47:02.011011-08:00","created_by":"mayor","updated_at":"2025-12-29T23:56:30.617961-08:00","dependencies":[{"issue_id":"bd-hj0s","depends_on_id":"bd-3roq","type":"blocks","created_at":"2025-12-29T18:47:10.59211-08:00","created_by":"daemon"}]}
|
{"id":"bd-hj0s","title":"Add 'convoy' issue type with reactive completion","description":"Add convoy as a new issue type with reactive completion semantics.\n\nBehavior:\n- Convoy has list of tracked issues (via 'tracks' relation)\n- When all tracked issues close (including wontfix), convoy auto-closes\n- Supports cross-prefix tracking (convoy in hq-* tracks gt-*, bd-*)\n\nImplementation:\n- New type: convoy\n- Reactive completion trigger on tracked issue closure\n- Query support: 'bd list --type=convoy'\n\nRelated: hq-7h8jx (Convoy System epic in town beads)","status":"closed","priority":1,"issue_type":"task","assignee":"beads/crew/dave","created_at":"2025-12-29T18:47:02.011011-08:00","created_by":"mayor","updated_at":"2025-12-30T00:05:13.515078-08:00","closed_at":"2025-12-30T00:05:13.515078-08:00","close_reason":"Implemented convoy type with reactive completion","dependencies":[{"issue_id":"bd-hj0s","depends_on_id":"bd-3roq","type":"blocks","created_at":"2025-12-29T18:47:10.59211-08:00","created_by":"daemon"}]}
|
||||||
{"id":"bd-hkr6","title":"GH#518: Document bd setup command","description":"bd setup is undiscoverable. Add to README/docs. Currently only findable by grepping source. See GitHub issue #518.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:54.664668-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"}
|
{"id":"bd-hkr6","title":"GH#518: Document bd setup command","description":"bd setup is undiscoverable. Add to README/docs. Currently only findable by grepping source. See GitHub issue #518.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:54.664668-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"}
|
||||||
{"id":"bd-hlsw","title":"Add sync resilience guardrails for forced pushes and prefix mismatches","description":"Beads can get into unrecoverable sync states when remote forces pushes occur (e.g., rebases) combined with prefix mismatches from multi-worker scenarios. Add detection, prevention, and auto-recovery features to handle this gracefully.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-14T10:40:14.872875259-07:00","updated_at":"2025-12-14T10:40:14.872875259-07:00"}
|
{"id":"bd-hlsw","title":"Add sync resilience guardrails for forced pushes and prefix mismatches","description":"Beads can get into unrecoverable sync states when remote forces pushes occur (e.g., rebases) combined with prefix mismatches from multi-worker scenarios. Add detection, prevention, and auto-recovery features to handle this gracefully.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-14T10:40:14.872875259-07:00","updated_at":"2025-12-14T10:40:14.872875259-07:00"}
|
||||||
{"id":"bd-hlsw.3","title":"Auto-recovery mode (bd sync --auto-recover)","description":"Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T10:40:20.599836875-07:00","updated_at":"2025-12-14T10:40:20.599836875-07:00","dependencies":[{"issue_id":"bd-hlsw.3","depends_on_id":"bd-hlsw","type":"parent-child","created_at":"2025-12-14T10:40:20.600435888-07:00","created_by":"daemon"}]}
|
{"id":"bd-hlsw.3","title":"Auto-recovery mode (bd sync --auto-recover)","description":"Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T10:40:20.599836875-07:00","updated_at":"2025-12-14T10:40:20.599836875-07:00","dependencies":[{"issue_id":"bd-hlsw.3","depends_on_id":"bd-hlsw","type":"parent-child","created_at":"2025-12-14T10:40:20.600435888-07:00","created_by":"daemon"}]}
|
||||||
|
|||||||
@@ -64,45 +64,103 @@ def _register_client_for_cleanup(client: BdClientBase) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_beads_redirect(beads_dir: str, workspace_root: str) -> str | None:
|
||||||
|
"""Follow a .beads/redirect file to the actual beads directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
beads_dir: Path to the .beads directory that may contain a redirect
|
||||||
|
workspace_root: The workspace root directory (parent of beads_dir)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resolved workspace root if redirect is valid, None otherwise
|
||||||
|
"""
|
||||||
|
import glob
|
||||||
|
|
||||||
|
redirect_path = os.path.join(beads_dir, "redirect")
|
||||||
|
if not os.path.isfile(redirect_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(redirect_path, 'r') as f:
|
||||||
|
redirect_target = f.read().strip()
|
||||||
|
|
||||||
|
if not redirect_target:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Resolve relative to workspace_root (the redirect is written from the perspective
|
||||||
|
# of being inside workspace_root, not inside workspace_root/.beads)
|
||||||
|
# e.g., redirect contains "../../mayor/rig/.beads"
|
||||||
|
# from polecats/capable/, this resolves to mayor/rig/.beads
|
||||||
|
resolved = os.path.normpath(os.path.join(workspace_root, redirect_target))
|
||||||
|
|
||||||
|
if not os.path.isdir(resolved):
|
||||||
|
logger.debug(f"Redirect target {resolved} does not exist")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Verify the redirected location has a valid database
|
||||||
|
db_files = glob.glob(os.path.join(resolved, "*.db"))
|
||||||
|
valid_dbs = [f for f in db_files if ".backup" not in os.path.basename(f)]
|
||||||
|
|
||||||
|
if not valid_dbs:
|
||||||
|
logger.debug(f"Redirect target {resolved} has no valid .db files")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return the workspace root of the redirected location (parent of .beads)
|
||||||
|
return os.path.dirname(resolved)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to follow redirect: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _find_beads_db_in_tree(start_dir: str | None = None) -> str | None:
|
def _find_beads_db_in_tree(start_dir: str | None = None) -> str | None:
|
||||||
"""Walk up directory tree looking for .beads/*.db (matches Go CLI behavior).
|
"""Walk up directory tree looking for .beads/*.db (matches Go CLI behavior).
|
||||||
|
|
||||||
|
Also follows .beads/redirect files to shared beads locations, which is
|
||||||
|
essential for polecat/crew directories that share a central database.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
start_dir: Starting directory (default: current working directory)
|
start_dir: Starting directory (default: current working directory)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Absolute path to workspace root containing .beads/*.db, or None if not found
|
Absolute path to workspace root containing .beads/*.db, or None if not found
|
||||||
"""
|
"""
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current = os.path.abspath(start_dir or os.getcwd())
|
current = os.path.abspath(start_dir or os.getcwd())
|
||||||
|
|
||||||
# Resolve symlinks like Go CLI does
|
# Resolve symlinks like Go CLI does
|
||||||
try:
|
try:
|
||||||
current = os.path.realpath(current)
|
current = os.path.realpath(current)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Walk up directory tree
|
# Walk up directory tree
|
||||||
while True:
|
while True:
|
||||||
beads_dir = os.path.join(current, ".beads")
|
beads_dir = os.path.join(current, ".beads")
|
||||||
if os.path.isdir(beads_dir):
|
if os.path.isdir(beads_dir):
|
||||||
# Find any .db file in .beads/ (excluding backups)
|
# First, check for redirect file (polecat/crew directories use this)
|
||||||
|
redirected = _resolve_beads_redirect(beads_dir, current)
|
||||||
|
if redirected:
|
||||||
|
logger.debug(f"Followed redirect from {current} to {redirected}")
|
||||||
|
return redirected
|
||||||
|
|
||||||
|
# No redirect, check for local .db files
|
||||||
db_files = glob.glob(os.path.join(beads_dir, "*.db"))
|
db_files = glob.glob(os.path.join(beads_dir, "*.db"))
|
||||||
valid_dbs = [f for f in db_files if ".backup" not in os.path.basename(f)]
|
valid_dbs = [f for f in db_files if ".backup" not in os.path.basename(f)]
|
||||||
|
|
||||||
if valid_dbs:
|
if valid_dbs:
|
||||||
# Return workspace root (parent of .beads), not the db path
|
# Return workspace root (parent of .beads), not the db path
|
||||||
return current
|
return current
|
||||||
|
|
||||||
parent = os.path.dirname(current)
|
parent = os.path.dirname(current)
|
||||||
if parent == current: # Reached filesystem root
|
if parent == current: # Reached filesystem root
|
||||||
break
|
break
|
||||||
current = parent
|
current = parent
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Failed to search for .beads in tree: {e}")
|
logger.debug(f"Failed to search for .beads in tree: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -135,22 +135,139 @@ async def test_get_client_prefers_context_var_over_auto_detect():
|
|||||||
current_workspace.reset(token)
|
current_workspace.reset(token)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_client_env_var_over_auto_detect():
|
async def test_get_client_env_var_over_auto_detect():
|
||||||
"""Test that BEADS_WORKING_DIR env var takes precedence over auto-detect."""
|
"""Test that BEADS_WORKING_DIR env var takes precedence over auto-detect."""
|
||||||
env_workspace = "/env/path"
|
env_workspace = "/env/path"
|
||||||
|
|
||||||
token = current_workspace.set(None)
|
token = current_workspace.set(None)
|
||||||
try:
|
try:
|
||||||
with patch.dict(os.environ, {"BEADS_WORKING_DIR": env_workspace}):
|
with patch.dict(os.environ, {"BEADS_WORKING_DIR": env_workspace}):
|
||||||
with patch("beads_mcp.tools._canonicalize_path", return_value=env_workspace):
|
with patch("beads_mcp.tools._canonicalize_path", return_value=env_workspace):
|
||||||
mock_client = AsyncMock()
|
mock_client = AsyncMock()
|
||||||
mock_client.ping = AsyncMock(return_value=None)
|
mock_client.ping = AsyncMock(return_value=None)
|
||||||
|
|
||||||
with patch("beads_mcp.tools.create_bd_client", return_value=mock_client):
|
with patch("beads_mcp.tools.create_bd_client", return_value=mock_client):
|
||||||
client = await _get_client()
|
client = await _get_client()
|
||||||
|
|
||||||
# Should use env var, not call auto-detect
|
# Should use env var, not call auto-detect
|
||||||
assert client is not None
|
assert client is not None
|
||||||
finally:
|
finally:
|
||||||
current_workspace.reset(token)
|
current_workspace.reset(token)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_beads_db_follows_redirect():
|
||||||
|
"""Test that _find_beads_db_in_tree follows .beads/redirect files (gt-tnw).
|
||||||
|
|
||||||
|
This is essential for polecat/crew directories that use redirect files
|
||||||
|
to share a central beads database.
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Create main workspace with actual database
|
||||||
|
main_dir = Path(tmpdir) / "mayor" / "rig"
|
||||||
|
main_dir.mkdir(parents=True)
|
||||||
|
main_beads = main_dir / ".beads"
|
||||||
|
main_beads.mkdir()
|
||||||
|
(main_beads / "beads.db").touch()
|
||||||
|
|
||||||
|
# Create polecat directory with redirect
|
||||||
|
polecat_dir = Path(tmpdir) / "polecats" / "capable"
|
||||||
|
polecat_dir.mkdir(parents=True)
|
||||||
|
polecat_beads = polecat_dir / ".beads"
|
||||||
|
polecat_beads.mkdir()
|
||||||
|
|
||||||
|
# Write redirect file (relative path from polecat dir)
|
||||||
|
redirect_file = polecat_beads / "redirect"
|
||||||
|
redirect_file.write_text("../../mayor/rig/.beads")
|
||||||
|
|
||||||
|
# Should find workspace via redirect
|
||||||
|
result = _find_beads_db_in_tree(str(polecat_dir))
|
||||||
|
assert result == os.path.realpath(str(main_dir))
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_beads_db_redirect_invalid_target():
|
||||||
|
"""Test that invalid redirect targets are handled gracefully."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Create polecat directory with redirect to nonexistent location
|
||||||
|
polecat_dir = Path(tmpdir) / "polecats" / "capable"
|
||||||
|
polecat_dir.mkdir(parents=True)
|
||||||
|
polecat_beads = polecat_dir / ".beads"
|
||||||
|
polecat_beads.mkdir()
|
||||||
|
|
||||||
|
# Write redirect file pointing to nonexistent location
|
||||||
|
redirect_file = polecat_beads / "redirect"
|
||||||
|
redirect_file.write_text("../../nonexistent/.beads")
|
||||||
|
|
||||||
|
# Should return None (graceful failure)
|
||||||
|
result = _find_beads_db_in_tree(str(polecat_dir))
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_beads_db_redirect_empty():
|
||||||
|
"""Test that empty redirect files are handled gracefully."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
polecat_dir = Path(tmpdir) / "polecats" / "capable"
|
||||||
|
polecat_dir.mkdir(parents=True)
|
||||||
|
polecat_beads = polecat_dir / ".beads"
|
||||||
|
polecat_beads.mkdir()
|
||||||
|
|
||||||
|
# Write empty redirect file
|
||||||
|
redirect_file = polecat_beads / "redirect"
|
||||||
|
redirect_file.write_text("")
|
||||||
|
|
||||||
|
# Should return None (graceful failure)
|
||||||
|
result = _find_beads_db_in_tree(str(polecat_dir))
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_beads_db_redirect_no_db_at_target():
|
||||||
|
"""Test that redirects to directories without .db files are handled."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Create main workspace WITHOUT database
|
||||||
|
main_dir = Path(tmpdir) / "mayor" / "rig"
|
||||||
|
main_dir.mkdir(parents=True)
|
||||||
|
main_beads = main_dir / ".beads"
|
||||||
|
main_beads.mkdir() # No .db file!
|
||||||
|
|
||||||
|
# Create polecat directory with redirect
|
||||||
|
polecat_dir = Path(tmpdir) / "polecats" / "capable"
|
||||||
|
polecat_dir.mkdir(parents=True)
|
||||||
|
polecat_beads = polecat_dir / ".beads"
|
||||||
|
polecat_beads.mkdir()
|
||||||
|
|
||||||
|
redirect_file = polecat_beads / "redirect"
|
||||||
|
redirect_file.write_text("../../mayor/rig/.beads")
|
||||||
|
|
||||||
|
# Should return None (redirect target has no valid db)
|
||||||
|
result = _find_beads_db_in_tree(str(polecat_dir))
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_beads_db_prefers_redirect_over_parent():
|
||||||
|
"""Test that redirect in current dir is followed before walking up."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Create two beads locations
|
||||||
|
# 1. Parent directory with its own database
|
||||||
|
parent_dir = Path(tmpdir)
|
||||||
|
parent_beads = parent_dir / ".beads"
|
||||||
|
parent_beads.mkdir()
|
||||||
|
(parent_beads / "beads.db").touch()
|
||||||
|
|
||||||
|
# 2. Remote directory that redirect points to
|
||||||
|
remote_dir = Path(tmpdir) / "remote"
|
||||||
|
remote_dir.mkdir()
|
||||||
|
remote_beads = remote_dir / ".beads"
|
||||||
|
remote_beads.mkdir()
|
||||||
|
(remote_beads / "beads.db").touch()
|
||||||
|
|
||||||
|
# Create child directory with redirect to remote
|
||||||
|
child_dir = Path(tmpdir) / "child"
|
||||||
|
child_dir.mkdir()
|
||||||
|
child_beads = child_dir / ".beads"
|
||||||
|
child_beads.mkdir()
|
||||||
|
redirect_file = child_beads / "redirect"
|
||||||
|
redirect_file.write_text("../remote/.beads")
|
||||||
|
|
||||||
|
# Should follow redirect (to remote), not walk up to parent
|
||||||
|
result = _find_beads_db_in_tree(str(child_dir))
|
||||||
|
assert result == os.path.realpath(str(remote_dir))
|
||||||
|
|||||||
Reference in New Issue
Block a user