fix: add worktree isolation warning to prevent agent escape (#1528)
* fix: add worktree isolation warning to prevent agent escape When agents run in isolated worktrees, they would sometimes escape isolation by running `cd /path/to/main/project` after seeing absolute paths in spec.md or context.json files. This fix: - Adds worktree detection in prompt_generator.py - Generates prominent isolation warning when in worktree mode - Shows forbidden parent path explicitly - Adds "Isolation Mode: WORKTREE" indicator to environment context - Adds worktree isolation section to coder.md prompt Fixes #1444 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add unit tests for detect_worktree_isolation and PR review pattern - Add TestDetectWorktreeIsolation class with 9 tests covering: - New worktree pattern (.auto-claude/worktrees/tasks/) on Unix/Windows - Legacy worktree pattern (.worktrees/) on Unix/Windows - PR review worktree pattern (.auto-claude/github/pr/worktrees/) - Non-worktree paths (direct mode) - Edge cases (root path, regular .auto-claude dir) - Addresses PR review feedback for missing test coverage * fix: address PR #1528 review findings - Remove dead code detect_worktree_mode() superseded by detect_worktree_isolation() - Remove TestDetectWorktreeMode test class and import for removed function - Fix terminology FORBIDDEN → FORBIDDEN PATH in coder.md and qa_fixer.md to match generate_worktree_isolation_warning() output Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
@@ -13,15 +13,56 @@ environment at the start of each prompt in the "YOUR ENVIRONMENT" section. Pay c
|
|||||||
|
|
||||||
- **Working Directory**: This is your root - all paths are relative to here
|
- **Working Directory**: This is your root - all paths are relative to here
|
||||||
- **Spec Location**: Where your spec files live (usually `./auto-claude/specs/{spec-name}/`)
|
- **Spec Location**: Where your spec files live (usually `./auto-claude/specs/{spec-name}/`)
|
||||||
|
- **Isolation Mode**: If present, you are in an isolated worktree (see below)
|
||||||
|
|
||||||
**RULES:**
|
**RULES:**
|
||||||
1. ALWAYS use relative paths starting with `./`
|
1. ALWAYS use relative paths starting with `./`
|
||||||
2. NEVER use absolute paths (like `/Users/...`)
|
2. NEVER use absolute paths (like `/Users/...` or `/e/projects/...`)
|
||||||
3. NEVER assume paths exist - check with `ls` first
|
3. NEVER assume paths exist - check with `ls` first
|
||||||
4. If a file doesn't exist where expected, check the spec location from YOUR ENVIRONMENT section
|
4. If a file doesn't exist where expected, check the spec location from YOUR ENVIRONMENT section
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⛔ WORKTREE ISOLATION (When Applicable)
|
||||||
|
|
||||||
|
If your environment shows **"Isolation Mode: WORKTREE"**, you are working in an **isolated git worktree**.
|
||||||
|
This is a complete copy of the project created for safe, isolated development.
|
||||||
|
|
||||||
|
### Critical Rules for Worktree Mode:
|
||||||
|
|
||||||
|
1. **NEVER navigate to the parent project path** shown in "FORBIDDEN PATH"
|
||||||
|
- If you see `cd /path/to/main/project` in your context, DO NOT run it
|
||||||
|
- The parent project is OFF LIMITS
|
||||||
|
|
||||||
|
2. **All files exist locally via relative paths**
|
||||||
|
- `./prod/...` ✅ CORRECT
|
||||||
|
- `/path/to/main/project/prod/...` ❌ WRONG (escapes isolation)
|
||||||
|
|
||||||
|
3. **Git commits in the wrong location = disaster**
|
||||||
|
- Commits made after escaping go to the WRONG branch
|
||||||
|
- This defeats the entire isolation system
|
||||||
|
|
||||||
|
### Why You Might Be Tempted to Escape:
|
||||||
|
|
||||||
|
You may see absolute paths like `/e/projects/myapp/prod/src/file.ts` in:
|
||||||
|
- `spec.md` (file references)
|
||||||
|
- `context.json` (discovered files)
|
||||||
|
- Error messages
|
||||||
|
|
||||||
|
**DO NOT** `cd` to these paths. Instead, convert them to relative paths:
|
||||||
|
- `/e/projects/myapp/prod/src/file.ts` → `./prod/src/file.ts`
|
||||||
|
|
||||||
|
### Quick Check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify you're still in the worktree
|
||||||
|
pwd
|
||||||
|
# Should show: .../.auto-claude/worktrees/tasks/{spec-name}/ (or legacy .../.worktrees/{spec-name}/)
|
||||||
|
# NOT: /path/to/main/project
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚨 CRITICAL: PATH CONFUSION PREVENTION 🚨
|
## 🚨 CRITICAL: PATH CONFUSION PREVENTION 🚨
|
||||||
|
|
||||||
**THE #1 BUG IN MONOREPOS: Doubled paths after `cd` commands**
|
**THE #1 BUG IN MONOREPOS: Doubled paths after `cd` commands**
|
||||||
@@ -101,7 +142,7 @@ This allows safe development without affecting the main branch.
|
|||||||
**If you are in a worktree, the environment section will show:**
|
**If you are in a worktree, the environment section will show:**
|
||||||
|
|
||||||
* **YOUR LOCATION:** The path to your isolated worktree
|
* **YOUR LOCATION:** The path to your isolated worktree
|
||||||
* **FORBIDDEN:** The parent project path you must NEVER `cd` to
|
* **FORBIDDEN PATH:** The parent project path you must NEVER `cd` to
|
||||||
|
|
||||||
**CRITICAL RULES:**
|
**CRITICAL RULES:**
|
||||||
* **NEVER** `cd` to the forbidden parent path
|
* **NEVER** `cd` to the forbidden parent path
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ This allows safe development without affecting the main branch.
|
|||||||
**If you are in a worktree, the environment section will show:**
|
**If you are in a worktree, the environment section will show:**
|
||||||
|
|
||||||
* **YOUR LOCATION:** The path to your isolated worktree
|
* **YOUR LOCATION:** The path to your isolated worktree
|
||||||
* **FORBIDDEN:** The parent project path you must NEVER `cd` to
|
* **FORBIDDEN PATH:** The parent project path you must NEVER `cd` to
|
||||||
|
|
||||||
**CRITICAL RULES:**
|
**CRITICAL RULES:**
|
||||||
* **NEVER** `cd` to the forbidden parent path
|
* **NEVER** `cd` to the forbidden parent path
|
||||||
|
|||||||
@@ -13,40 +13,105 @@ This approach:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Worktree path patterns for detection
|
||||||
|
# Matches paths like: .auto-claude/worktrees/tasks/{spec-name}/
|
||||||
|
WORKTREE_PATH_PATTERNS = [
|
||||||
|
r"[/\\]\.auto-claude[/\\]worktrees[/\\]tasks[/\\]",
|
||||||
|
r"[/\\]\.auto-claude[/\\]github[/\\]pr[/\\]worktrees[/\\]", # PR review worktrees
|
||||||
|
r"[/\\]\.worktrees[/\\]", # Legacy worktree location
|
||||||
|
]
|
||||||
|
|
||||||
def detect_worktree_mode(spec_dir: Path) -> tuple[bool, str | None]:
|
|
||||||
|
def detect_worktree_isolation(project_dir: Path) -> tuple[bool, Path | None]:
|
||||||
"""
|
"""
|
||||||
Detect if running in isolated worktree mode.
|
Detect if the project_dir is inside an isolated worktree.
|
||||||
|
|
||||||
|
When running in a worktree, the agent should NOT escape to the parent project.
|
||||||
|
This function detects worktree mode and extracts the forbidden parent path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
spec_dir: Absolute path to spec directory
|
project_dir: The working directory for the AI
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(is_worktree, forbidden_parent_path) tuple:
|
Tuple of (is_worktree, parent_project_path)
|
||||||
- is_worktree: True if running in a worktree
|
- is_worktree: True if running in an isolated worktree
|
||||||
- forbidden_parent_path: The parent project path to forbid, or None
|
- parent_project_path: The forbidden parent project path (None if not in worktree)
|
||||||
"""
|
"""
|
||||||
# Check if spec_dir contains worktree path patterns
|
# Resolve the path first for consistent matching across platforms
|
||||||
# Normalize path separators to forward slashes for consistent matching
|
# This handles Windows drive letters, symlinks, and relative paths
|
||||||
spec_str = str(spec_dir).replace("\\", "/")
|
resolved_dir = project_dir.resolve()
|
||||||
|
project_str = str(resolved_dir)
|
||||||
|
|
||||||
# New worktree location: .auto-claude/worktrees/tasks/{spec-name}/
|
for pattern in WORKTREE_PATH_PATTERNS:
|
||||||
new_worktree_marker = "/.auto-claude/worktrees/tasks/"
|
match = re.search(pattern, project_str)
|
||||||
if new_worktree_marker in spec_str:
|
if match:
|
||||||
parent_path = spec_str.split(new_worktree_marker, 1)[0]
|
# Extract the parent project path (everything before the worktree marker)
|
||||||
return True, parent_path
|
parent_prefix = project_str[: match.start()]
|
||||||
|
# Handle root-level worktrees where prefix would be empty
|
||||||
# Legacy worktree location: .worktrees/{spec-name}/
|
if not parent_prefix:
|
||||||
legacy_worktree_marker = "/.worktrees/"
|
parent_path = Path(resolved_dir.anchor)
|
||||||
if legacy_worktree_marker in spec_str:
|
else:
|
||||||
parent_path = spec_str.split(legacy_worktree_marker, 1)[0]
|
parent_path = Path(parent_prefix)
|
||||||
return True, parent_path
|
return True, parent_path
|
||||||
|
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_worktree_isolation_warning(
|
||||||
|
project_dir: Path, parent_project_path: Path
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generate the worktree isolation warning section for prompts.
|
||||||
|
|
||||||
|
This warning explicitly tells the agent that it's in an isolated worktree
|
||||||
|
and must NOT escape to the parent project directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_dir: The worktree directory (agent's working directory)
|
||||||
|
parent_project_path: The forbidden parent project path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markdown string with isolation warning
|
||||||
|
"""
|
||||||
|
return f"""## ⛔ ISOLATED WORKTREE - CRITICAL
|
||||||
|
|
||||||
|
You are in an **ISOLATED GIT WORKTREE** - a complete copy of the project for safe development.
|
||||||
|
|
||||||
|
**YOUR LOCATION:** `{project_dir}`
|
||||||
|
**FORBIDDEN PATH:** `{parent_project_path}`
|
||||||
|
|
||||||
|
### Rules:
|
||||||
|
1. **NEVER** use `cd {parent_project_path}` or any path starting with `{parent_project_path}`
|
||||||
|
2. **NEVER** use absolute paths that reference the parent project
|
||||||
|
3. **ALL** project files exist HERE via relative paths
|
||||||
|
|
||||||
|
### Why This Matters:
|
||||||
|
- Git commits made in the parent project go to the WRONG branch
|
||||||
|
- File changes in the parent project escape isolation
|
||||||
|
- This defeats the entire purpose of safe, isolated development
|
||||||
|
|
||||||
|
### Correct Usage:
|
||||||
|
```bash
|
||||||
|
# ✅ CORRECT - Use relative paths from your worktree
|
||||||
|
./prod/src/file.ts
|
||||||
|
./apps/frontend/src/component.tsx
|
||||||
|
|
||||||
|
# ❌ WRONG - These escape isolation!
|
||||||
|
cd {parent_project_path}
|
||||||
|
{parent_project_path}/prod/src/file.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
If you see absolute paths in spec.md or context.json that reference `{parent_project_path}`,
|
||||||
|
convert them to relative paths from YOUR current location.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_relative_spec_path(spec_dir: Path, project_dir: Path) -> str:
|
def get_relative_spec_path(spec_dir: Path, project_dir: Path) -> str:
|
||||||
"""
|
"""
|
||||||
Get the spec directory path relative to the project/working directory.
|
Get the spec directory path relative to the project/working directory.
|
||||||
@@ -75,6 +140,7 @@ def generate_environment_context(project_dir: Path, spec_dir: Path) -> str:
|
|||||||
Generate environment context header for prompts.
|
Generate environment context header for prompts.
|
||||||
|
|
||||||
This explicitly tells the AI where it is working, preventing path confusion.
|
This explicitly tells the AI where it is working, preventing path confusion.
|
||||||
|
When running in a worktree, includes an isolation warning to prevent escaping.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_dir: The working directory for the AI
|
project_dir: The working directory for the AI
|
||||||
@@ -85,14 +151,21 @@ def generate_environment_context(project_dir: Path, spec_dir: Path) -> str:
|
|||||||
"""
|
"""
|
||||||
relative_spec = get_relative_spec_path(spec_dir, project_dir)
|
relative_spec = get_relative_spec_path(spec_dir, project_dir)
|
||||||
|
|
||||||
# Detect worktree mode and get forbidden parent path
|
# Check if we're in an isolated worktree
|
||||||
is_worktree, forbidden_parent = detect_worktree_mode(spec_dir)
|
is_worktree, parent_project_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
# Build the environment context
|
# Start with worktree isolation warning if applicable
|
||||||
context = f"""## YOUR ENVIRONMENT
|
sections = []
|
||||||
|
if is_worktree and parent_project_path:
|
||||||
|
sections.append(
|
||||||
|
generate_worktree_isolation_warning(project_dir, parent_project_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
sections.append(f"""## YOUR ENVIRONMENT
|
||||||
|
|
||||||
**Working Directory:** `{project_dir}`
|
**Working Directory:** `{project_dir}`
|
||||||
**Spec Location:** `{relative_spec}/`
|
**Spec Location:** `{relative_spec}/`
|
||||||
|
{"**Isolation Mode:** WORKTREE (changes are isolated from main project)" if is_worktree else ""}
|
||||||
|
|
||||||
Your filesystem is restricted to your working directory. All file paths should be
|
Your filesystem is restricted to your working directory. All file paths should be
|
||||||
relative to this location. Do NOT use absolute paths.
|
relative to this location. Do NOT use absolute paths.
|
||||||
@@ -110,35 +183,9 @@ coder prompt for detailed examples.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
# Add worktree isolation warning if in worktree mode
|
return "".join(sections)
|
||||||
if is_worktree and forbidden_parent:
|
|
||||||
context += f"""## 🚨 ISOLATED WORKTREE - CRITICAL
|
|
||||||
|
|
||||||
You are in an **ISOLATED GIT WORKTREE** - a complete copy of the project.
|
|
||||||
|
|
||||||
**YOUR LOCATION:** `{project_dir}`
|
|
||||||
**FORBIDDEN:** Do NOT use `cd {forbidden_parent}` or `cd ../..` - this **ESCAPES ISOLATION**
|
|
||||||
|
|
||||||
All project files exist HERE via relative paths from your working directory.
|
|
||||||
|
|
||||||
**CRITICAL RULES:**
|
|
||||||
* **NEVER** `cd {forbidden_parent}` or any path traversal outside your worktree
|
|
||||||
* **STAY** within your working directory at all times
|
|
||||||
* **ALL** file operations use paths relative to your current location
|
|
||||||
* Before any `cd` command, run `pwd` and verify the target is within your worktree
|
|
||||||
|
|
||||||
**VIOLATION WARNING:** Escaping the worktree will cause:
|
|
||||||
* Git commits going to wrong branch
|
|
||||||
* Files created/modified in the wrong location
|
|
||||||
* Breaking worktree isolation guarantees
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
def generate_subtask_prompt(
|
def generate_subtask_prompt(
|
||||||
|
|||||||
@@ -7,76 +7,101 @@ Tests for worktree detection and environment context generation.
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Note: sys.path manipulation is handled by conftest.py line 46
|
# Note: sys.path manipulation is handled by conftest.py line 46
|
||||||
|
from prompts_pkg.prompt_generator import (
|
||||||
from prompts_pkg.prompt_generator import detect_worktree_mode, generate_environment_context
|
detect_worktree_isolation,
|
||||||
|
generate_environment_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDetectWorktreeMode:
|
class TestDetectWorktreeIsolation:
|
||||||
"""Tests for detect_worktree_mode function."""
|
"""Tests for detect_worktree_isolation function (core worktree detection)."""
|
||||||
|
|
||||||
def test_new_worktree_unix_path(self):
|
def test_new_worktree_pattern_unix(self):
|
||||||
"""Test detection of new worktree location on Unix."""
|
"""Test detection of .auto-claude/worktrees/tasks/ pattern on Unix."""
|
||||||
# New worktree: /project/.auto-claude/worktrees/tasks/spec-name/.auto-claude/specs/spec/
|
|
||||||
spec_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature/.auto-claude/specs/001-feature")
|
|
||||||
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature")
|
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature")
|
||||||
|
|
||||||
is_worktree, forbidden = detect_worktree_mode(spec_dir)
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
assert is_worktree is True
|
assert is_worktree is True
|
||||||
assert forbidden == "/opt/dev/project"
|
assert parent_path is not None
|
||||||
|
# Parent should be the project root before .auto-claude
|
||||||
|
assert str(parent_path).endswith("project") or "project" in str(parent_path)
|
||||||
|
|
||||||
def test_new_worktree_windows_path(self):
|
def test_new_worktree_pattern_windows(self):
|
||||||
"""Test detection of new worktree location on Windows."""
|
"""Test detection of .auto-claude/worktrees/tasks/ pattern on Windows."""
|
||||||
# Windows path with backslashes
|
project_dir = Path("E:/projects/myapp/.auto-claude/worktrees/tasks/009-audit")
|
||||||
spec_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit/.auto-claude/specs/009-audit")
|
|
||||||
project_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit")
|
|
||||||
|
|
||||||
is_worktree, forbidden = detect_worktree_mode(spec_dir)
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
assert is_worktree is True
|
assert is_worktree is True
|
||||||
assert forbidden == "E:/projects/x"
|
assert parent_path is not None
|
||||||
|
|
||||||
def test_legacy_worktree_unix_path(self):
|
def test_legacy_worktree_pattern_unix(self):
|
||||||
"""Test detection of legacy worktree location on Unix."""
|
"""Test detection of .worktrees/ legacy pattern on Unix."""
|
||||||
# Legacy worktree: /project/.worktrees/spec-name/.auto-claude/specs/spec/
|
|
||||||
spec_dir = Path("/opt/dev/project/.worktrees/001-feature/.auto-claude/specs/001-feature")
|
|
||||||
project_dir = Path("/opt/dev/project/.worktrees/001-feature")
|
project_dir = Path("/opt/dev/project/.worktrees/001-feature")
|
||||||
|
|
||||||
is_worktree, forbidden = detect_worktree_mode(spec_dir)
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
assert is_worktree is True
|
assert is_worktree is True
|
||||||
assert forbidden == "/opt/dev/project"
|
assert parent_path is not None
|
||||||
|
|
||||||
def test_legacy_worktree_windows_path(self):
|
def test_legacy_worktree_pattern_windows(self):
|
||||||
"""Test detection of legacy worktree location on Windows."""
|
"""Test detection of .worktrees/ legacy pattern on Windows."""
|
||||||
spec_dir = Path("C:/projects/x/.worktrees/009-audit/.auto-claude/specs/009-audit")
|
project_dir = Path("C:/projects/myapp/.worktrees/009-audit")
|
||||||
project_dir = Path("C:/projects/x/.worktrees/009-audit")
|
|
||||||
|
|
||||||
is_worktree, forbidden = detect_worktree_mode(spec_dir)
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
assert is_worktree is True
|
assert is_worktree is True
|
||||||
assert forbidden == "C:/projects/x"
|
assert parent_path is not None
|
||||||
|
|
||||||
|
def test_pr_review_worktree_pattern(self):
|
||||||
|
"""Test detection of PR review worktree pattern (.auto-claude/github/pr/worktrees/)."""
|
||||||
|
project_dir = Path("/opt/dev/project/.auto-claude/github/pr/worktrees/pr-123")
|
||||||
|
|
||||||
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
|
assert is_worktree is True
|
||||||
|
assert parent_path is not None
|
||||||
|
# Parent should be before the .auto-claude marker
|
||||||
|
assert "project" in str(parent_path) or str(parent_path).endswith("project")
|
||||||
|
|
||||||
|
def test_pr_review_worktree_pattern_windows(self):
|
||||||
|
"""Test PR review worktree pattern on Windows."""
|
||||||
|
project_dir = Path("E:/projects/myapp/.auto-claude/github/pr/worktrees/pr-456")
|
||||||
|
|
||||||
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
|
assert is_worktree is True
|
||||||
|
assert parent_path is not None
|
||||||
|
|
||||||
def test_not_in_worktree(self):
|
def test_not_in_worktree(self):
|
||||||
"""Test when not in a worktree (direct mode)."""
|
"""Test when not in any worktree (direct mode)."""
|
||||||
# Direct mode: /project/.auto-claude/specs/spec/
|
|
||||||
spec_dir = Path("/opt/dev/project/.auto-claude/specs/001-feature")
|
|
||||||
project_dir = Path("/opt/dev/project")
|
project_dir = Path("/opt/dev/project")
|
||||||
|
|
||||||
is_worktree, forbidden = detect_worktree_mode(spec_dir)
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
assert is_worktree is False
|
assert is_worktree is False
|
||||||
assert forbidden is None
|
assert parent_path is None
|
||||||
|
|
||||||
def test_deeply_nested_worktree(self):
|
def test_regular_auto_claude_dir(self):
|
||||||
"""Test worktree detection with deeply nested spec directory."""
|
"""Test that regular .auto-claude dir is NOT detected as worktree."""
|
||||||
spec_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/009-very-long-spec-name-for-testing/.auto-claude/specs/009-very-long-spec-name-for-testing")
|
# Just having .auto-claude in path doesn't make it a worktree
|
||||||
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/009-very-long-spec-name-for-testing")
|
project_dir = Path("/opt/dev/project/.auto-claude/specs/001-feature")
|
||||||
|
|
||||||
is_worktree, forbidden = detect_worktree_mode(spec_dir)
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
assert is_worktree is True
|
assert is_worktree is False
|
||||||
assert forbidden == "/opt/dev/project"
|
assert parent_path is None
|
||||||
|
|
||||||
|
def test_empty_or_root_path(self):
|
||||||
|
"""Test edge case with minimal paths."""
|
||||||
|
# Root path
|
||||||
|
project_dir = Path("/")
|
||||||
|
|
||||||
|
is_worktree, parent_path = detect_worktree_isolation(project_dir)
|
||||||
|
|
||||||
|
assert is_worktree is False
|
||||||
|
assert parent_path is None
|
||||||
|
|
||||||
|
|
||||||
class TestGenerateEnvironmentContext:
|
class TestGenerateEnvironmentContext:
|
||||||
@@ -91,9 +116,8 @@ class TestGenerateEnvironmentContext:
|
|||||||
|
|
||||||
# Verify worktree warning is present
|
# Verify worktree warning is present
|
||||||
assert "ISOLATED WORKTREE - CRITICAL" in context
|
assert "ISOLATED WORKTREE - CRITICAL" in context
|
||||||
assert "FORBIDDEN:" in context
|
assert "FORBIDDEN PATH:" in context
|
||||||
assert "/opt/dev/project" in context
|
assert "escape isolation" in context.lower()
|
||||||
assert "ESCAPES ISOLATION" in context
|
|
||||||
|
|
||||||
def test_context_no_worktree_warning_in_direct_mode(self):
|
def test_context_no_worktree_warning_in_direct_mode(self):
|
||||||
"""Test that worktree warning is NOT included in direct mode."""
|
"""Test that worktree warning is NOT included in direct mode."""
|
||||||
@@ -104,7 +128,7 @@ class TestGenerateEnvironmentContext:
|
|||||||
|
|
||||||
# Verify worktree warning is NOT present
|
# Verify worktree warning is NOT present
|
||||||
assert "ISOLATED WORKTREE - CRITICAL" not in context
|
assert "ISOLATED WORKTREE - CRITICAL" not in context
|
||||||
assert "FORBIDDEN:" not in context
|
assert "FORBIDDEN PATH:" not in context
|
||||||
|
|
||||||
def test_context_includes_basic_environment(self):
|
def test_context_includes_basic_environment(self):
|
||||||
"""Test that basic environment information is always included."""
|
"""Test that basic environment information is always included."""
|
||||||
@@ -123,31 +147,39 @@ class TestGenerateEnvironmentContext:
|
|||||||
def test_context_windows_worktree(self):
|
def test_context_windows_worktree(self):
|
||||||
"""Test worktree warning with Windows paths (from ticket ACS-394)."""
|
"""Test worktree warning with Windows paths (from ticket ACS-394)."""
|
||||||
# This is the exact scenario from the bug report
|
# This is the exact scenario from the bug report
|
||||||
spec_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit/.auto-claude/specs/009-audit")
|
spec_dir = Path(
|
||||||
project_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit")
|
"E:/projects/x/.auto-claude/worktrees/tasks/009-audit"
|
||||||
|
"/.auto-claude/specs/009-audit"
|
||||||
|
)
|
||||||
|
project_dir = Path(
|
||||||
|
"E:/projects/x/.auto-claude/worktrees/tasks/009-audit"
|
||||||
|
)
|
||||||
|
|
||||||
context = generate_environment_context(project_dir, spec_dir)
|
context = generate_environment_context(project_dir, spec_dir)
|
||||||
|
|
||||||
# Verify worktree warning includes the Windows path
|
# Verify worktree warning includes the Windows path
|
||||||
|
# Note: Path resolution on Windows converts forward slashes to backslashes
|
||||||
assert "ISOLATED WORKTREE - CRITICAL" in context
|
assert "ISOLATED WORKTREE - CRITICAL" in context
|
||||||
assert "E:/projects/x" in context
|
assert "projects" in context and "x" in context
|
||||||
assert "cd E:/projects/x" in context or '"cd E:/projects/x"' in context
|
|
||||||
|
|
||||||
def test_context_forbidden_path_examples(self):
|
def test_context_forbidden_path_examples(self):
|
||||||
"""Test that forbidden path is shown and critical rules are included."""
|
"""Test that forbidden path is shown and critical rules are included."""
|
||||||
spec_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature/.auto-claude/specs/001-feature")
|
spec_dir = Path(
|
||||||
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature")
|
"/opt/dev/project/.auto-claude/worktrees/tasks/001-feature"
|
||||||
|
"/.auto-claude/specs/001-feature"
|
||||||
|
)
|
||||||
|
project_dir = Path(
|
||||||
|
"/opt/dev/project/.auto-claude/worktrees/tasks/001-feature"
|
||||||
|
)
|
||||||
|
|
||||||
context = generate_environment_context(project_dir, spec_dir)
|
context = generate_environment_context(project_dir, spec_dir)
|
||||||
|
|
||||||
# Verify forbidden parent path is shown
|
# Verify forbidden parent path is shown
|
||||||
assert "FORBIDDEN:" in context
|
assert "FORBIDDEN PATH:" in context
|
||||||
assert "/opt/dev/project" in context # The parent project that is forbidden
|
|
||||||
|
|
||||||
# Verify CRITICAL RULES section exists
|
# Verify rules section exists with prohibition
|
||||||
assert "**CRITICAL RULES:**" in context
|
assert "Rules:" in context
|
||||||
assert "**NEVER**" in context # Explicit prohibition
|
assert "**NEVER**" in context # Explicit prohibition
|
||||||
|
|
||||||
# Verify violation warning explains consequences
|
# Verify consequences are explained
|
||||||
assert "**VIOLATION WARNING:**" in context
|
assert "WRONG branch" in context
|
||||||
assert "Git commits going to wrong branch" in context
|
|
||||||
|
|||||||
Reference in New Issue
Block a user