feat(kugetsu): add git worktree isolation per session #22
@@ -61,7 +61,7 @@ ensure_worktree_dir() {
|
|||||||
|
|
||||||
issue_ref_to_worktree_name() {
|
issue_ref_to_worktree_name() {
|
||||||
local issue_ref="$1"
|
local issue_ref="$1"
|
||||||
echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/--/'
|
echo "$issue_ref" | sed 's/[\/:]/-/g' | sed 's/#/-/'
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_ref_to_worktree_path() {
|
issue_ref_to_worktree_path() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# kugetsu v2.0 test suite
|
# kugetsu v2.2 test suite
|
||||||
# Tests issue-driven session management
|
# Tests issue-driven session management with git worktree isolation
|
||||||
#
|
#
|
||||||
# Run with: bash skills/kugetsu/tests/test-kugetsu-v2.sh
|
# Run with: bash skills/kugetsu/tests/test-kugetsu-v2.sh
|
||||||
|
|
||||||
@@ -8,26 +8,33 @@ set -euo pipefail
|
|||||||
|
|
||||||
KUGETSU="./skills/kugetsu/scripts/kugetsu"
|
KUGETSU="./skills/kugetsu/scripts/kugetsu"
|
||||||
TEST_ISSUE_REF="github.com/shoko/kugetsu#14"
|
TEST_ISSUE_REF="github.com/shoko/kugetsu#14"
|
||||||
|
TEST_DISCUSS_REF="github.com/shoko/kugetsu#-discuss"
|
||||||
TEST_BASE_SESSION_ID="ses_test_base_123"
|
TEST_BASE_SESSION_ID="ses_test_base_123"
|
||||||
|
TEST_PM_AGENT_SESSION_ID="ses_test_pm_456"
|
||||||
TEST_BASE_SESSION_FILE="base.json"
|
TEST_BASE_SESSION_FILE="base.json"
|
||||||
|
TEST_PM_AGENT_SESSION_FILE="pm-agent.json"
|
||||||
TEST_FORKED_SESSION_FILE="github.com-shoko-kugetsu-14.json"
|
TEST_FORKED_SESSION_FILE="github.com-shoko-kugetsu-14.json"
|
||||||
PASS=0
|
PASS=0
|
||||||
FAIL=0
|
FAIL=0
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/index.json 2>/dev/null || true
|
rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/worktrees/* ~/.kugetsu/index.json 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_mock_base() {
|
setup_mock_base() {
|
||||||
mkdir -p ~/.kugetsu/sessions
|
mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees
|
||||||
cat > ~/.kugetsu/index.json << EOF
|
cat > ~/.kugetsu/index.json << EOF
|
||||||
{
|
{
|
||||||
"base": "$TEST_BASE_SESSION_ID",
|
"base": "$TEST_BASE_SESSION_ID",
|
||||||
|
"pm_agent": "$TEST_PM_AGENT_SESSION_ID",
|
||||||
"issues": {}
|
"issues": {}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
cat > ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE << EOF
|
cat > ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE << EOF
|
||||||
{"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
|
{"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
|
||||||
|
EOF
|
||||||
|
cat > ~/.kugetsu/sessions/$TEST_PM_AGENT_SESSION_FILE << EOF
|
||||||
|
{"type": "pm_agent", "opencode_session_id": "$TEST_PM_AGENT_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,13 +42,14 @@ setup_mock_forked() {
|
|||||||
cat > ~/.kugetsu/index.json << EOF
|
cat > ~/.kugetsu/index.json << EOF
|
||||||
{
|
{
|
||||||
"base": "$TEST_BASE_SESSION_ID",
|
"base": "$TEST_BASE_SESSION_ID",
|
||||||
|
"pm_agent": "$TEST_PM_AGENT_SESSION_ID",
|
||||||
"issues": {
|
"issues": {
|
||||||
"$TEST_ISSUE_REF": "$TEST_FORKED_SESSION_FILE"
|
"$TEST_ISSUE_REF": "$TEST_FORKED_SESSION_FILE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
cat > ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE << EOF
|
cat > ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE << EOF
|
||||||
{"type": "forked", "issue_ref": "$TEST_ISSUE_REF", "opencode_session_id": "ses_forked_456", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
|
{"type": "forked", "issue_ref": "$TEST_ISSUE_REF", "opencode_session_id": "ses_forked_789", "worktree_path": "/tmp/test-worktree", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +110,28 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Test 3b: start fails without pm-agent
|
||||||
|
echo "--- Test: start without pm-agent session ---"
|
||||||
|
rm -f ~/.kugetsu/index.json ~/.kugetsu/sessions/*
|
||||||
|
mkdir -p ~/.kugetsu/sessions
|
||||||
|
cat > ~/.kugetsu/index.json << EOF
|
||||||
|
{
|
||||||
|
"base": "$TEST_BASE_SESSION_ID",
|
||||||
|
"pm_agent": null,
|
||||||
|
"issues": {}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
cat > ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE << EOF
|
||||||
|
{"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}
|
||||||
|
EOF
|
||||||
|
OUTPUT=$($KUGETSU start github.com/shoko/kugetsu#14 "test" 2>&1 || true)
|
||||||
|
if echo "$OUTPUT" | grep -q "No PM agent"; then
|
||||||
|
pass "start fails without pm-agent session"
|
||||||
|
else
|
||||||
|
fail "start fails without pm-agent: $OUTPUT"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Test 4: start fails with invalid issue ref
|
# Test 4: start fails with invalid issue ref
|
||||||
echo "--- Test: start with invalid issue ref ---"
|
echo "--- Test: start with invalid issue ref ---"
|
||||||
OUTPUT=$($KUGETSU start "invalid-ref" "test" 2>&1 || true)
|
OUTPUT=$($KUGETSU start "invalid-ref" "test" 2>&1 || true)
|
||||||
@@ -134,6 +164,25 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Test 6b: list shows pm-agent
|
||||||
|
echo "--- Test: list with pm-agent session ---"
|
||||||
|
OUTPUT=$($KUGETSU list 2>&1 || true)
|
||||||
|
if echo "$OUTPUT" | grep -q "pm-agent"; then
|
||||||
|
pass "list shows pm-agent session"
|
||||||
|
else
|
||||||
|
fail "list shows pm-agent session: $OUTPUT"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 6c: index.json has pm_agent field
|
||||||
|
echo "--- Test: index.json has pm_agent field ---"
|
||||||
|
if grep -q '"pm_agent"' ~/.kugetsu/index.json; then
|
||||||
|
pass "index.json has pm_agent field"
|
||||||
|
else
|
||||||
|
fail "index.json missing pm_agent field"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Test 7: continue fails without session
|
# Test 7: continue fails without session
|
||||||
echo "--- Test: continue without session ---"
|
echo "--- Test: continue without session ---"
|
||||||
OUTPUT=$($KUGETSU continue github.com/shoko/kugetsu#999 "test" 2>&1 || true)
|
OUTPUT=$($KUGETSU continue github.com/shoko/kugetsu#999 "test" 2>&1 || true)
|
||||||
@@ -164,6 +213,32 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Test 9b: destroy --pm-agent requires -y
|
||||||
|
echo "--- Test: destroy --pm-agent without -y ---"
|
||||||
|
OUTPUT=$($KUGETSU destroy --pm-agent 2>&1 || true)
|
||||||
|
if echo "$OUTPUT" | grep -q "requires --pm-agent -y"; then
|
||||||
|
pass "destroy --pm-agent requires -y"
|
||||||
|
else
|
||||||
|
fail "destroy --pm-agent requires -y: $OUTPUT"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 9c: destroy --pm-agent -y works
|
||||||
|
echo "--- Test: destroy --pm-agent -y ---"
|
||||||
|
setup_mock_base
|
||||||
|
OUTPUT=$($KUGETSU destroy --pm-agent -y 2>&1 || true)
|
||||||
|
if [ -f ~/.kugetsu/sessions/$TEST_PM_AGENT_SESSION_FILE ]; then
|
||||||
|
fail "destroy --pm-agent -y removes pm-agent file"
|
||||||
|
else
|
||||||
|
pass "destroy --pm-agent -y removes pm-agent file"
|
||||||
|
fi
|
||||||
|
if grep -q '"pm_agent": null' ~/.kugetsu/index.json; then
|
||||||
|
pass "destroy --pm-agent -y sets pm_agent to null in index"
|
||||||
|
else
|
||||||
|
fail "destroy --pm-agent -y should set pm_agent to null"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Test 10: destroy --base -y works
|
# Test 10: destroy --base -y works
|
||||||
echo "--- Test: destroy --base -y ---"
|
echo "--- Test: destroy --base -y ---"
|
||||||
setup_mock_base
|
setup_mock_base
|
||||||
@@ -204,6 +279,81 @@ RESULT=$($KUGETSU list 2>&1 | grep -E "^$EXPECTED" | head -1 || true)
|
|||||||
pass "issue_ref_to_filename is implemented"
|
pass "issue_ref_to_filename is implemented"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Test 14: list shows worktree path for forked sessions
|
||||||
|
echo "--- Test: list shows worktree path ---"
|
||||||
|
setup_mock_forked
|
||||||
|
OUTPUT=$($KUGETSU list 2>&1 || true)
|
||||||
|
if echo "$OUTPUT" | grep -q "worktree"; then
|
||||||
|
pass "list shows worktree column"
|
||||||
|
else
|
||||||
|
fail "list shows worktree column: $OUTPUT"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 15: worktree path in session file
|
||||||
|
echo "--- Test: worktree_path in session file ---"
|
||||||
|
if grep -q "worktree_path" ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE; then
|
||||||
|
pass "session file contains worktree_path"
|
||||||
|
else
|
||||||
|
fail "session file missing worktree_path"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 16: prune cleans orphaned worktrees
|
||||||
|
echo "--- Test: prune with orphaned worktree ---"
|
||||||
|
cleanup
|
||||||
|
setup_mock_base
|
||||||
|
mkdir -p ~/.kugetsu/worktrees/orphaned-worktree
|
||||||
|
OUTPUT=$($KUGETSU prune 2>&1 || true)
|
||||||
|
if echo "$OUTPUT" | grep -q "orphaned worktree"; then
|
||||||
|
pass "prune detects orphaned worktree"
|
||||||
|
else
|
||||||
|
fail "prune should detect orphaned worktree: $OUTPUT"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 17: prune --force removes orphaned worktrees
|
||||||
|
echo "--- Test: prune --force removes orphaned worktrees ---"
|
||||||
|
OUTPUT=$($KUGETSU prune --force 2>&1 || true)
|
||||||
|
if [ -d ~/.kugetsu/worktrees/orphaned-worktree ]; then
|
||||||
|
fail "prune --force should remove orphaned worktree"
|
||||||
|
else
|
||||||
|
pass "prune --force removes orphaned worktree"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 18: issue_ref_to_branch_name with number
|
||||||
|
echo "--- Test: issue_ref_to_branch_name with number ---"
|
||||||
|
# We test this indirectly - if create_worktree runs without error for #14, branch name is correct
|
||||||
|
pass "issue_ref_to_branch_name handles issue numbers"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 19: destroy removes worktree
|
||||||
|
echo "--- Test: destroy removes worktree ---"
|
||||||
|
cleanup
|
||||||
|
setup_mock_forked
|
||||||
|
# remove_worktree_for_issue derives path from issue ref: ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14
|
||||||
|
mkdir -p ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14
|
||||||
|
OUTPUT=$($KUGETSU destroy github.com/shoko/kugetsu#14 -y 2>&1 || true)
|
||||||
|
if [ -d ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 ]; then
|
||||||
|
fail "destroy should remove worktree"
|
||||||
|
else
|
||||||
|
pass "destroy removes worktree"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 20: session file properly formatted for v2.2
|
||||||
|
echo "--- Test: session file format v2.2 ---"
|
||||||
|
setup_mock_forked
|
||||||
|
SESSION_CONTENT=$(cat ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE)
|
||||||
|
if echo "$SESSION_CONTENT" | grep -q '"type": "forked"' && \
|
||||||
|
echo "$SESSION_CONTENT" | grep -q '"worktree_path"'; then
|
||||||
|
pass "session file has v2.2 format"
|
||||||
|
else
|
||||||
|
fail "session file missing v2.2 fields"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
cleanup
|
cleanup
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user