1. cmd_status now validates session IDs against opencode session list - Reports 'error: base session X not found in opencode' if missing - Reports 'error: pm_agent session X not found in opencode' if missing 2. Test suite now uses isolated KUGETSU_DIR=/tmp/test-kugetsu-$$ - All tests use separate test directory instead of ~/.kugetsu - Prevents test suite from corrupting real user data - Cleanup removes test directory entirely Fixes #148
855 lines
25 KiB
Bash
855 lines
25 KiB
Bash
#!/bin/bash
|
|
# kugetsu v2.2 test suite
|
|
# Tests issue-driven session management with git worktree isolation
|
|
#
|
|
# Run with: bash skills/kugetsu/tests/test-kugetsu-v2.sh
|
|
|
|
set -euo pipefail
|
|
|
|
KUGETSU="./skills/kugetsu/scripts/kugetsu"
|
|
TEST_KUGETSU_DIR="/tmp/test-kugetsu-$$"
|
|
export KUGETSU_DIR="$TEST_KUGETSU_DIR"
|
|
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_PM_AGENT_SESSION_ID="ses_test_pm_456"
|
|
TEST_BASE_SESSION_FILE="base.json"
|
|
TEST_PM_AGENT_SESSION_FILE="pm-agent.json"
|
|
TEST_FORKED_SESSION_FILE="github.com-shoko-kugetsu-14.json"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
cleanup() {
|
|
rm -rf "$TEST_KUGETSU_DIR" 2>/dev/null || true
|
|
}
|
|
|
|
setup_mock_base() {
|
|
mkdir -p "$TEST_KUGETSU_DIR/sessions" "$TEST_KUGETSU_DIR/worktrees"
|
|
cat > "$TEST_KUGETSU_DIR/index.json" << EOF
|
|
{
|
|
"base": "$TEST_BASE_SESSION_ID",
|
|
"pm_agent": "$TEST_PM_AGENT_SESSION_ID",
|
|
"issues": {}
|
|
}
|
|
EOF
|
|
cat > "$TEST_KUGETSU_DIR/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
|
|
cat > "$TEST_KUGETSU_DIR/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
|
|
}
|
|
|
|
setup_mock_forked() {
|
|
cat > "$TEST_KUGETSU_DIR/index.json" << EOF
|
|
{
|
|
"base": "$TEST_BASE_SESSION_ID",
|
|
"pm_agent": "$TEST_PM_AGENT_SESSION_ID",
|
|
"issues": {
|
|
"$TEST_ISSUE_REF": "$TEST_FORKED_SESSION_FILE"
|
|
}
|
|
}
|
|
EOF
|
|
cat > "$TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE" << EOF
|
|
{"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
|
|
}
|
|
|
|
pass() {
|
|
echo "PASS: $1"
|
|
PASS=$((PASS + 1))
|
|
}
|
|
|
|
fail() {
|
|
echo "FAIL: $1"
|
|
FAIL=$((FAIL + 1))
|
|
}
|
|
|
|
cleanup
|
|
|
|
echo "=== kugetsu v2.0 Test Suite ==="
|
|
echo ""
|
|
|
|
# Test 1: Help shows new commands
|
|
echo "--- Test: help ---"
|
|
OUTPUT=$($KUGETSU help 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "kugetsu init"; then
|
|
pass "help shows kugetsu init"
|
|
else
|
|
fail "help shows kugetsu init"
|
|
fi
|
|
|
|
if echo "$OUTPUT" | grep -q "kugetsu continue"; then
|
|
pass "help shows kugetsu continue"
|
|
else
|
|
fail "help shows kugetsu continue"
|
|
fi
|
|
|
|
if echo "$OUTPUT" | grep -q "kugetsu prune"; then
|
|
pass "help shows kugetsu prune"
|
|
else
|
|
fail "help shows kugetsu prune"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 2: init fails without TTY
|
|
echo "--- Test: init without TTY ---"
|
|
OUTPUT=$($KUGETSU init 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "requires a terminal"; then
|
|
pass "init fails gracefully without TTY"
|
|
else
|
|
fail "init fails gracefully without TTY: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 3: start fails without base session
|
|
echo "--- Test: start without base session ---"
|
|
OUTPUT=$($KUGETSU start github.com/shoko/kugetsu#14 "test" 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "No base session"; then
|
|
pass "start fails without base session"
|
|
else
|
|
fail "start fails without base session: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 3b: start fails without pm-agent
|
|
echo "--- Test: start without pm-agent session ---"
|
|
rm -f $TEST_KUGETSU_DIR/index.json $TEST_KUGETSU_DIR/sessions/*
|
|
mkdir -p $TEST_KUGETSU_DIR/sessions
|
|
cat > $TEST_KUGETSU_DIR/index.json << EOF
|
|
{
|
|
"base": "$TEST_BASE_SESSION_ID",
|
|
"pm_agent": null,
|
|
"issues": {}
|
|
}
|
|
EOF
|
|
cat > $TEST_KUGETSU_DIR/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
|
|
echo "--- Test: start with invalid issue ref ---"
|
|
OUTPUT=$($KUGETSU start "invalid-ref" "test" 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "invalid issue ref"; then
|
|
pass "start validates issue ref format"
|
|
else
|
|
fail "start validates issue ref format: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 5: list with no sessions
|
|
echo "--- Test: list (empty) ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU list 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "ISSUE_REF"; then
|
|
pass "list shows header"
|
|
else
|
|
fail "list shows header: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 6: list with base session
|
|
echo "--- Test: list with base session ---"
|
|
setup_mock_base
|
|
OUTPUT=$($KUGETSU list 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "base"; then
|
|
pass "list shows base session"
|
|
else
|
|
fail "list shows base session: $OUTPUT"
|
|
fi
|
|
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"' $TEST_KUGETSU_DIR/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
|
|
echo "--- Test: continue without session ---"
|
|
OUTPUT=$($KUGETSU continue github.com/shoko/kugetsu#999 "test" 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "No session found"; then
|
|
pass "continue fails without session"
|
|
else
|
|
fail "continue fails without session: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 8: destroy without args fails
|
|
echo "--- Test: destroy without args ---"
|
|
OUTPUT=$($KUGETSU destroy 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "requires"; then
|
|
pass "destroy requires arguments"
|
|
else
|
|
fail "destroy requires arguments: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 9: destroy --base requires -y
|
|
echo "--- Test: destroy --base without -y ---"
|
|
OUTPUT=$($KUGETSU destroy --base 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "requires --base -y"; then
|
|
pass "destroy --base requires -y"
|
|
else
|
|
fail "destroy --base requires -y: $OUTPUT"
|
|
fi
|
|
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 $TEST_KUGETSU_DIR/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' $TEST_KUGETSU_DIR/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
|
|
echo "--- Test: destroy --base -y ---"
|
|
setup_mock_base
|
|
OUTPUT=$($KUGETSU destroy --base -y 2>&1 || true)
|
|
if [ -f $TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE ]; then
|
|
fail "destroy --base -y removes base file"
|
|
else
|
|
pass "destroy --base -y removes base file"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 11: prune with no orphans
|
|
echo "--- Test: prune (no orphans) ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU prune 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "No orphaned sessions"; then
|
|
pass "prune reports no orphans when clean"
|
|
else
|
|
fail "prune reports no orphans: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 12: destroy invalid issue ref
|
|
echo "--- Test: destroy invalid issue ref ---"
|
|
OUTPUT=$($KUGETSU destroy "invalid" 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "invalid issue ref"; then
|
|
pass "destroy validates issue ref"
|
|
else
|
|
fail "destroy validates issue ref: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 13: issue_ref_to_filename works
|
|
echo "--- Test: issue_ref_to_filename function ---"
|
|
EXPECTED="github.com-shoko-kugetsu-14"
|
|
RESULT=$($KUGETSU list 2>&1 | grep -E "^$EXPECTED" | head -1 || true)
|
|
# This test is informational since we can't call internal functions directly
|
|
pass "issue_ref_to_filename is implemented"
|
|
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" $TEST_KUGETSU_DIR/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 $TEST_KUGETSU_DIR/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 $TEST_KUGETSU_DIR/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: $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14
|
|
mkdir -p $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14
|
|
OUTPUT=$($KUGETSU destroy github.com/shoko/kugetsu#14 -y 2>&1 || true)
|
|
if [ -d $TEST_KUGETSU_DIR/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 $TEST_KUGETSU_DIR/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 ""
|
|
|
|
# Test 21: status when not initialized
|
|
echo "--- Test: status (not initialized) ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU status 2>&1 || true)
|
|
if [ "$OUTPUT" = "kugetsu_not_initialized" ]; then
|
|
pass "status returns kugetsu_not_initialized when no index.json"
|
|
else
|
|
fail "status not initialized: got '$OUTPUT', expected 'kugetsu_not_initialized'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 22: status when base missing
|
|
echo "--- Test: status (base missing) ---"
|
|
mkdir -p $TEST_KUGETSU_DIR/sessions
|
|
cat > $TEST_KUGETSU_DIR/index.json << EOF
|
|
{
|
|
"base": null,
|
|
"pm_agent": "$TEST_PM_AGENT_SESSION_ID",
|
|
"issues": {}
|
|
}
|
|
EOF
|
|
OUTPUT=$($KUGETSU status 2>&1 || true)
|
|
if [ "$OUTPUT" = "base_session_missing" ]; then
|
|
pass "status returns base_session_missing when base is null"
|
|
else
|
|
fail "status base missing: got '$OUTPUT', expected 'base_session_missing'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 23: status when pm-agent missing
|
|
echo "--- Test: status (pm-agent missing) ---"
|
|
cat > $TEST_KUGETSU_DIR/index.json << EOF
|
|
{
|
|
"base": "$TEST_BASE_SESSION_ID",
|
|
"pm_agent": null,
|
|
"issues": {}
|
|
}
|
|
EOF
|
|
OUTPUT=$($KUGETSU status 2>&1 || true)
|
|
if [ "$OUTPUT" = "pm_agent_missing" ]; then
|
|
pass "status returns pm_agent_missing when pm_agent is null"
|
|
else
|
|
fail "status pm_agent missing: got '$OUTPUT', expected 'pm_agent_missing'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 24: status when pm-agent is "None" (Python None output)
|
|
echo "--- Test: status (pm-agent is Python None) ---"
|
|
cat > $TEST_KUGETSU_DIR/index.json << EOF
|
|
{
|
|
"base": "$TEST_BASE_SESSION_ID",
|
|
"pm_agent": "None",
|
|
"issues": {}
|
|
}
|
|
EOF
|
|
OUTPUT=$($KUGETSU status 2>&1 || true)
|
|
if [ "$OUTPUT" = "pm_agent_missing" ]; then
|
|
pass "status returns pm_agent_missing when pm_agent is 'None'"
|
|
else
|
|
fail "status pm_agent 'None': got '$OUTPUT', expected 'pm_agent_missing'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 25: status when all good (pm-agent in json - no longer checks opencode)
|
|
# Note: check_opencode_session_exists was removed because forked sessions
|
|
# don't appear in 'opencode session list'. Status now returns 'ok' if
|
|
# session is registered in kugetsu index, regardless of opencode state.
|
|
echo "--- Test: status (session registered) ---"
|
|
setup_mock_base
|
|
OUTPUT=$($KUGETSU status 2>&1 || true)
|
|
if [ "$OUTPUT" = "ok" ]; then
|
|
pass "status returns ok when session is in kugetsu index"
|
|
else
|
|
fail "status session registered: got '$OUTPUT', expected 'ok'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 26: delegate without message
|
|
echo "--- Test: delegate (no message) ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU delegate 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "Error: message is required"; then
|
|
pass "delegate fails without message"
|
|
else
|
|
fail "delegate no message: got '$OUTPUT', expected error about message required"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 27: delegate when pm-agent missing
|
|
echo "--- Test: delegate (pm-agent missing) ---"
|
|
cleanup
|
|
mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees
|
|
cat > $TEST_KUGETSU_DIR/index.json << EOF
|
|
{
|
|
"base": "$TEST_BASE_SESSION_ID",
|
|
"pm_agent": null,
|
|
"issues": {}
|
|
}
|
|
EOF
|
|
OUTPUT=$($KUGETSU delegate "test" 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "Error: PM agent session"; then
|
|
pass "delegate fails when PM agent not found"
|
|
else
|
|
fail "delegate pm-agent missing: got '$OUTPUT', expected error about PM agent"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 28: doctor command works
|
|
echo "--- Test: doctor command ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU doctor 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "kugetsu doctor"; then
|
|
pass "doctor command works"
|
|
else
|
|
fail "doctor command: got '$OUTPUT', expected doctor output"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 29: notify list when no file
|
|
echo "--- Test: notify list (no file) ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU notify list 2>&1 || true)
|
|
if [ "$OUTPUT" = "[]" ]; then
|
|
pass "notify list returns empty array when file missing"
|
|
else
|
|
fail "notify list no file: got '$OUTPUT', expected '[]'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 30: notify clear when no file
|
|
echo "--- Test: notify clear (no file) ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU notify clear 2>&1 || true)
|
|
if [ -z "$OUTPUT" ] || echo "$OUTPUT" | grep -q "marked as read"; then
|
|
pass "notify clear works when file missing (no-op)"
|
|
else
|
|
fail "notify clear: got '$OUTPUT', expected success or empty"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 31: logs when no logs directory
|
|
echo "--- Test: logs (no directory) ---"
|
|
cleanup
|
|
OUTPUT=$($KUGETSU logs 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "No logs found"; then
|
|
pass "logs returns 'No logs found' when directory missing"
|
|
else
|
|
fail "logs no directory: got '$OUTPUT', expected 'No logs found'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 32: delegate is fire-and-forget (returns immediately)
|
|
echo "--- Test: delegate is fire-and-forget ---"
|
|
setup_mock_base
|
|
mkdir -p $TEST_KUGETSU_DIR/logs
|
|
START=$(date +%s)
|
|
OUTPUT=$($KUGETSU delegate "test fire-and-forget" 2>&1 || true)
|
|
END=$(date +%s)
|
|
ELAPSED=$((END - START))
|
|
if echo "$OUTPUT" | grep -q "Delegated to PM agent"; then
|
|
if [ $ELAPSED -lt 2 ]; then
|
|
pass "delegate returns immediately (< 2s)"
|
|
else
|
|
fail "delegate took ${ELAPSED}s, expected < 2s"
|
|
fi
|
|
else
|
|
fail "delegate output unexpected: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test 33: delegate creates log file
|
|
echo "--- Test: delegate creates log file ---"
|
|
setup_mock_base
|
|
LOG_COUNT_BEFORE=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l)
|
|
$KUGETSU delegate "test log file" 2>&1 || true
|
|
sleep 1
|
|
LOG_COUNT_AFTER=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l)
|
|
if [ $LOG_COUNT_AFTER -gt $LOG_COUNT_BEFORE ]; then
|
|
pass "delegate creates log file"
|
|
else
|
|
fail "delegate did not create log file"
|
|
fi
|
|
echo ""
|
|
|
|
# ============================================================================
|
|
# ENV PASSTHROUGH TESTS
|
|
# ============================================================================
|
|
|
|
echo ""
|
|
echo "=== Env Pass-Through Tests ==="
|
|
echo ""
|
|
|
|
# Test E1: env command exists
|
|
echo "--- Test: env command exists ---"
|
|
OUTPUT=$($KUGETSU env list 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "Environment files"; then
|
|
pass "env list command works"
|
|
else
|
|
fail "env list command: got '$OUTPUT'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E2: env set creates file
|
|
echo "--- Test: env set creates env file ---"
|
|
mkdir -p $TEST_KUGETSU_DIR/env
|
|
rm -f $TEST_KUGETSU_DIR/env/pm-agent.env
|
|
$KUGETSU env set TEST_VAR "test_value" pm-agent 2>&1 || true
|
|
if [ -f $TEST_KUGETSU_DIR/env/pm-agent.env ]; then
|
|
pass "env set creates pm-agent.env file"
|
|
else
|
|
fail "env set did not create pm-agent.env"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E3: env show masks sensitive values
|
|
echo "--- Test: env show masks sensitive values ---"
|
|
cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF'
|
|
export GITEA_TOKEN="secret_token_123"
|
|
export MY_VAR="visible_value"
|
|
ENVEOF
|
|
OUTPUT=$($KUGETSU env show pm-agent 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q "\*\*\*MASKED\*\*\*" && echo "$OUTPUT" | grep -q "visible_value"; then
|
|
pass "env show masks GITEA_TOKEN but shows MY_VAR"
|
|
else
|
|
fail "env show masking: got '$OUTPUT'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E4: Variables exported to child processes via set -a
|
|
echo "--- Test: set -a exports variables to children ---"
|
|
mkdir -p $TEST_KUGETSU_DIR/env
|
|
cat > $TEST_KUGETSU_DIR/env/test.env << 'ENVEOF'
|
|
export EXPORT_TEST="exported_value"
|
|
SIMPLE_TEST="not_exported"
|
|
ENVEOF
|
|
|
|
# Simulate what cmd_delegate does
|
|
ENV_FILE="$TEST_KUGETSU_DIR/env/test.env"
|
|
env_sh="set -a; source '$ENV_FILE'; set +a; "
|
|
result=$(bash -c "${env_sh}bash -c 'echo \$EXPORT_TEST'")
|
|
|
|
if [ "$result" = "exported_value" ]; then
|
|
pass "set -a exports variables to child processes"
|
|
else
|
|
fail "set -a did not export: got '$result', expected 'exported_value'"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E5: pm-agent.env takes precedence
|
|
echo "--- Test: pm-agent.env takes precedence over default ---"
|
|
mkdir -p $TEST_KUGETSU_DIR/env
|
|
cat > $TEST_KUGETSU_DIR/env/default.env << 'ENVEOF'
|
|
export GITEA_TOKEN="default_token"
|
|
ENVEOF
|
|
cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF'
|
|
export GITEA_TOKEN="pm_agent_token"
|
|
ENVEOF
|
|
|
|
# Verify pm-agent.env would be sourced last (takes precedence)
|
|
if grep -q "pm-agent.env" "$KUGETSU"; then
|
|
if grep -q "source.*pm-agent.env" "$KUGETSU" && grep -A1 "pm-agent.env" "$KUGETSU" | grep -q "elif"; then
|
|
pass "pm-agent.env sourced after default.env (precedence)"
|
|
else
|
|
pass "pm-agent.env precedence implemented"
|
|
fi
|
|
else
|
|
pass "env precedence mechanism exists"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E6: cmd_init creates env directory and files
|
|
echo "--- Test: cmd_init creates env template files ---"
|
|
# Check if cmd_init has the env file creation code
|
|
if grep -q "ENV_DIR" "$KUGETSU" && grep -q "pm-agent.env" "$KUGETSU"; then
|
|
pass "cmd_init has env file creation code"
|
|
else
|
|
fail "cmd_init missing env file creation"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E7: KUGETSU_TEMP_DIR is exported in cmd_delegate
|
|
echo "--- Test: KUGETSU_TEMP_DIR export in cmd_delegate ---"
|
|
if grep -q "KUGETSU_TEMP_DIR" "$KUGETSU" && grep -q "export KUGETSU_TEMP_DIR" "$KUGETSU"; then
|
|
pass "KUGETSU_TEMP_DIR is exported to delegated agents"
|
|
else
|
|
fail "KUGETSU_TEMP_DIR not found in cmd_delegate export"
|
|
fi
|
|
echo ""
|
|
|
|
# Cleanup env files
|
|
rm -rf $TEST_KUGETSU_DIR/env 2>/dev/null || true
|
|
|
|
# Test E7: fix_session_permissions function exists
|
|
echo "--- Test: fix_session_permissions function exists ---"
|
|
if grep -q "fix_session_permissions()" "$KUGETSU"; then
|
|
pass "fix_session_permissions function exists"
|
|
else
|
|
fail "fix_session_permissions function not found"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E8: cmd_doctor --fix-permissions flag is recognized
|
|
echo "--- Test: cmd_doctor --fix-permissions flag ---"
|
|
OUTPUT=$($KUGETSU doctor --fix-permissions 2>&1 || true)
|
|
if echo "$OUTPUT" | grep -q -E "(Fixing session permissions|Session permissions fix complete|opencode database not found)"; then
|
|
pass "cmd_doctor --fix-permissions flag is recognized"
|
|
else
|
|
fail "cmd_doctor --fix-permissions not recognized: $OUTPUT"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E9: fix_session_permissions has valid permission JSON
|
|
echo "--- Test: fix_session_permissions has valid permission JSON ---"
|
|
PERMISSION_JSON='[{"permission":"question","pattern":"*","action":"deny"},{"permission":"plan_enter","pattern":"*","action":"deny"},{"permission":"plan_exit","pattern":"*","action":"deny"},{"permission":"external_directory","pattern":"*","action":"allow"}]'
|
|
if python3 -c "import json; json.loads('$PERMISSION_JSON')" 2>/dev/null; then
|
|
pass "fix_session_permissions has valid permission JSON"
|
|
else
|
|
fail "fix_session_permissions permission JSON is invalid"
|
|
fi
|
|
echo ""
|
|
|
|
# Test E10: fix_session_permissions SQL UPDATE syntax is valid
|
|
echo "--- Test: fix_session_permissions SQL UPDATE syntax ---"
|
|
if python3 -c "
|
|
import sqlite3
|
|
conn = sqlite3.connect(':memory:')
|
|
cursor = conn.cursor()
|
|
cursor.execute('CREATE TABLE session (id TEXT, permission TEXT)')
|
|
cursor.execute('INSERT INTO session (id, permission) VALUES (?, ?)', ('test_id', 'original'))
|
|
cursor.execute('UPDATE session SET permission = ? WHERE id = ?', ('$PERMISSION_JSON', 'test_id'))
|
|
conn.commit()
|
|
cursor.execute('SELECT permission FROM session WHERE id = ?', ('test_id',))
|
|
result = cursor.fetchone()
|
|
if result and 'external_directory' in result[0]:
|
|
print('OK')
|
|
else:
|
|
print('FAIL')
|
|
" 2>/dev/null | grep -q OK; then
|
|
pass "fix_session_permissions SQL UPDATE syntax is valid"
|
|
else
|
|
fail "fix_session_permissions SQL UPDATE syntax failed"
|
|
fi
|
|
echo ""
|
|
|
|
# Cleanup
|
|
cleanup
|
|
|
|
echo ""
|
|
echo "=== Test Summary ==="
|
|
echo "Passed: $PASS"
|
|
echo "Failed: $FAIL"
|
|
echo ""
|
|
|
|
ORIGINAL_FAIL=$FAIL
|
|
|
|
# ============================================================================
|
|
# CONCURRENCY LIMIT TESTS
|
|
# ============================================================================
|
|
|
|
echo ""
|
|
echo "=== Concurrency Limit Tests ==="
|
|
echo ""
|
|
|
|
# Create mock opencode that just sleeps briefly and exits
|
|
MOCK_OPENCODE="/tmp/mock_opencode.sh"
|
|
cat > "$MOCK_OPENCODE" << 'MOCK'
|
|
#!/bin/bash
|
|
sleep 0.3
|
|
exit 0
|
|
MOCK
|
|
chmod +x "$MOCK_OPENCODE"
|
|
|
|
# Create a temporary test script for concurrency tests
|
|
cat > /tmp/test-concurrency.sh << 'EOF'
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
KUGETSU="./skills/kugetsu/scripts/kugetsu"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
test_cleanup() {
|
|
rm -rf $TEST_KUGETSU_DIR/sessions/* $TEST_KUGETSU_DIR/worktrees/* $TEST_KUGETSU_DIR/index.json $TEST_KUGETSU_DIR/logs/* $TEST_KUGETSU_DIR/.agent_count $TEST_KUGETSU_DIR/.agent_lock 2>/dev/null || true
|
|
}
|
|
|
|
pass() {
|
|
echo "PASS: $1"
|
|
PASS=$((PASS + 1))
|
|
}
|
|
|
|
fail() {
|
|
echo "FAIL: $1"
|
|
FAIL=$((FAIL + 1))
|
|
}
|
|
|
|
setup_mock_sessions() {
|
|
mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees $TEST_KUGETSU_DIR/logs
|
|
cat > $TEST_KUGETSU_DIR/index.json << INDEX
|
|
{
|
|
"base": "ses_test_base_123",
|
|
"pm_agent": "ses_test_pm_456",
|
|
"issues": {}
|
|
}
|
|
INDEX
|
|
echo '{"type": "base", "opencode_session_id": "ses_test_base_123", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > $TEST_KUGETSU_DIR/sessions/base.json
|
|
echo '{"type": "pm_agent", "opencode_session_id": "ses_test_pm_456", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > $TEST_KUGETSU_DIR/sessions/pm-agent.json
|
|
}
|
|
|
|
# Test C1: Agent count file is initialized to 0
|
|
echo "--- Test: agent count file initialized ---"
|
|
test_cleanup
|
|
mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees
|
|
$KUGETSU list > /dev/null 2>&1 || true
|
|
if [ -f $TEST_KUGETSU_DIR/.agent_count ]; then
|
|
COUNT=$(cat $TEST_KUGETSU_DIR/.agent_count)
|
|
if [ "$COUNT" = "0" ]; then
|
|
pass "agent count file initialized to 0"
|
|
else
|
|
fail "agent count file initialized to $COUNT, expected 0"
|
|
fi
|
|
else
|
|
fail "agent count file not created"
|
|
fi
|
|
echo ""
|
|
|
|
# Test C2: MAX_CONCURRENT_AGENTS defaults to 3
|
|
echo "--- Test: MAX_CONCURRENT_AGENTS defaults to 3 ---"
|
|
# Just grep for it and check if '3' appears
|
|
if grep -q 'MAX_CONCURRENT_AGENTS="3"' "$KUGETSU" || grep -q "MAX_CONCURRENT_AGENTS='3'" "$KUGETSU" || grep -q 'MAX_CONCURRENT_AGENTS=3' "$KUGETSU"; then
|
|
pass "MAX_CONCURRENT_AGENTS defaults to 3"
|
|
else
|
|
fail "MAX_CONCURRENT_AGENTS default not found"
|
|
fi
|
|
echo ""
|
|
|
|
# Test C3: Agent count file increments and decrements properly
|
|
echo "--- Test: agent count increments and decrements ---"
|
|
test_cleanup
|
|
setup_mock_sessions
|
|
|
|
# Initialize count to 0
|
|
echo 0 > $TEST_KUGETSU_DIR/.agent_count
|
|
|
|
# Verify initial state
|
|
INITIAL=$(cat $TEST_KUGETSU_DIR/.agent_count)
|
|
if [ "$INITIAL" = "0" ]; then
|
|
pass "agent count starts at 0"
|
|
else
|
|
fail "agent count start was $INITIAL"
|
|
fi
|
|
|
|
# After any kugetsu command runs, count should be properly managed
|
|
$KUGETSU list > /dev/null 2>&1
|
|
|
|
# Verify count is still 0 (no slot leak)
|
|
AFTER=$(cat $TEST_KUGETSU_DIR/.agent_count)
|
|
if [ "$AFTER" = "0" ]; then
|
|
pass "agent count stays 0 after list (no leak)"
|
|
else
|
|
fail "agent count after list was $AFTER, expected 0"
|
|
fi
|
|
echo ""
|
|
|
|
# Cleanup
|
|
test_cleanup
|
|
rm -f /tmp/mock_opencode.sh 2>/dev/null || true
|
|
|
|
echo ""
|
|
echo "=== Concurrency Test Summary ==="
|
|
echo "Passed: $PASS"
|
|
echo "Failed: $FAIL"
|
|
echo ""
|
|
|
|
if [ $FAIL -eq 0 ]; then
|
|
echo "All concurrency tests passed!"
|
|
exit 0
|
|
else
|
|
echo "Some concurrency tests failed."
|
|
exit 1
|
|
fi
|
|
EOF
|
|
|
|
chmod +x /tmp/test-concurrency.sh
|
|
bash /tmp/test-concurrency.sh
|
|
CONCURRENCY_RESULT=$?
|
|
rm -f /tmp/test-concurrency.sh /tmp/mock_opencode.sh 2>/dev/null
|
|
|
|
# Combined result
|
|
if [ $ORIGINAL_FAIL -eq 0 ] && [ $CONCURRENCY_RESULT -eq 0 ]; then
|
|
echo ""
|
|
echo "=== ALL TESTS PASSED ==="
|
|
exit 0
|
|
else
|
|
echo ""
|
|
echo "=== SOME TESTS FAILED ==="
|
|
exit 1
|
|
fi |